sift-stack-py 0.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. google/__init__.py +1 -0
  2. google/api/__init__.py +0 -0
  3. google/api/annotations_pb2.py +27 -0
  4. google/api/annotations_pb2.pyi +29 -0
  5. google/api/annotations_pb2_grpc.py +4 -0
  6. google/api/annotations_pb2_grpc.pyi +30 -0
  7. google/api/field_behavior_pb2.py +30 -0
  8. google/api/field_behavior_pb2.pyi +175 -0
  9. google/api/field_behavior_pb2_grpc.py +4 -0
  10. google/api/field_behavior_pb2_grpc.pyi +30 -0
  11. google/api/http_pb2.py +31 -0
  12. google/api/http_pb2.pyi +433 -0
  13. google/api/http_pb2_grpc.py +4 -0
  14. google/api/http_pb2_grpc.pyi +30 -0
  15. protoc_gen_openapiv2/__init__.py +0 -0
  16. protoc_gen_openapiv2/options/__init__.py +0 -0
  17. protoc_gen_openapiv2/options/annotations_pb2.py +27 -0
  18. protoc_gen_openapiv2/options/annotations_pb2.pyi +48 -0
  19. protoc_gen_openapiv2/options/annotations_pb2_grpc.py +4 -0
  20. protoc_gen_openapiv2/options/annotations_pb2_grpc.pyi +17 -0
  21. protoc_gen_openapiv2/options/openapiv2_pb2.py +132 -0
  22. protoc_gen_openapiv2/options/openapiv2_pb2.pyi +1533 -0
  23. protoc_gen_openapiv2/options/openapiv2_pb2_grpc.py +4 -0
  24. protoc_gen_openapiv2/options/openapiv2_pb2_grpc.pyi +17 -0
  25. sift/__init__.py +0 -0
  26. sift/annotation_logs/__init__.py +0 -0
  27. sift/annotation_logs/v1/__init__.py +0 -0
  28. sift/annotation_logs/v1/annotation_logs_pb2.py +115 -0
  29. sift/annotation_logs/v1/annotation_logs_pb2.pyi +370 -0
  30. sift/annotation_logs/v1/annotation_logs_pb2_grpc.py +135 -0
  31. sift/annotation_logs/v1/annotation_logs_pb2_grpc.pyi +84 -0
  32. sift/annotations/__init__.py +0 -0
  33. sift/annotations/v1/__init__.py +0 -0
  34. sift/annotations/v1/annotations_pb2.py +180 -0
  35. sift/annotations/v1/annotations_pb2.pyi +539 -0
  36. sift/annotations/v1/annotations_pb2_grpc.py +237 -0
  37. sift/annotations/v1/annotations_pb2_grpc.pyi +144 -0
  38. sift/assets/__init__.py +0 -0
  39. sift/assets/v1/__init__.py +0 -0
  40. sift/assets/v1/assets_pb2.py +90 -0
  41. sift/assets/v1/assets_pb2.pyi +235 -0
  42. sift/assets/v1/assets_pb2_grpc.py +168 -0
  43. sift/assets/v1/assets_pb2_grpc.pyi +101 -0
  44. sift/calculated_channels/__init__.py +0 -0
  45. sift/calculated_channels/v1/__init__.py +0 -0
  46. sift/calculated_channels/v1/calculated_channels_pb2.py +99 -0
  47. sift/calculated_channels/v1/calculated_channels_pb2.pyi +280 -0
  48. sift/calculated_channels/v1/calculated_channels_pb2_grpc.py +101 -0
  49. sift/calculated_channels/v1/calculated_channels_pb2_grpc.pyi +64 -0
  50. sift/campaigns/__init__.py +0 -0
  51. sift/campaigns/v1/__init__.py +0 -0
  52. sift/campaigns/v1/campaigns_pb2.py +144 -0
  53. sift/campaigns/v1/campaigns_pb2.pyi +383 -0
  54. sift/campaigns/v1/campaigns_pb2_grpc.py +169 -0
  55. sift/campaigns/v1/campaigns_pb2_grpc.pyi +104 -0
  56. sift/channel_schemas/__init__.py +0 -0
  57. sift/channel_schemas/v1/__init__.py +0 -0
  58. sift/channel_schemas/v1/channel_schemas_pb2.py +69 -0
  59. sift/channel_schemas/v1/channel_schemas_pb2.pyi +117 -0
  60. sift/channel_schemas/v1/channel_schemas_pb2_grpc.py +101 -0
  61. sift/channel_schemas/v1/channel_schemas_pb2_grpc.pyi +64 -0
  62. sift/channels/__init__.py +0 -0
  63. sift/channels/v2/__init__.py +0 -0
  64. sift/channels/v2/channels_pb2.py +88 -0
  65. sift/channels/v2/channels_pb2.pyi +183 -0
  66. sift/channels/v2/channels_pb2_grpc.py +101 -0
  67. sift/channels/v2/channels_pb2_grpc.pyi +64 -0
  68. sift/common/__init__.py +0 -0
  69. sift/common/type/__init__.py +0 -0
  70. sift/common/type/v1/__init__.py +0 -0
  71. sift/common/type/v1/channel_bit_field_element_pb2.py +34 -0
  72. sift/common/type/v1/channel_bit_field_element_pb2.pyi +33 -0
  73. sift/common/type/v1/channel_bit_field_element_pb2_grpc.py +4 -0
  74. sift/common/type/v1/channel_bit_field_element_pb2_grpc.pyi +17 -0
  75. sift/common/type/v1/channel_data_type_pb2.py +29 -0
  76. sift/common/type/v1/channel_data_type_pb2.pyi +50 -0
  77. sift/common/type/v1/channel_data_type_pb2_grpc.py +4 -0
  78. sift/common/type/v1/channel_data_type_pb2_grpc.pyi +17 -0
  79. sift/common/type/v1/channel_enum_type_pb2.py +32 -0
  80. sift/common/type/v1/channel_enum_type_pb2.pyi +29 -0
  81. sift/common/type/v1/channel_enum_type_pb2_grpc.py +4 -0
  82. sift/common/type/v1/channel_enum_type_pb2_grpc.pyi +17 -0
  83. sift/common/type/v1/organization_pb2.py +27 -0
  84. sift/common/type/v1/organization_pb2.pyi +29 -0
  85. sift/common/type/v1/organization_pb2_grpc.py +4 -0
  86. sift/common/type/v1/organization_pb2_grpc.pyi +17 -0
  87. sift/common/type/v1/resource_identifier_pb2.py +46 -0
  88. sift/common/type/v1/resource_identifier_pb2.pyi +145 -0
  89. sift/common/type/v1/resource_identifier_pb2_grpc.py +4 -0
  90. sift/common/type/v1/resource_identifier_pb2_grpc.pyi +17 -0
  91. sift/common/type/v1/user_pb2.py +33 -0
  92. sift/common/type/v1/user_pb2.pyi +36 -0
  93. sift/common/type/v1/user_pb2_grpc.py +4 -0
  94. sift/common/type/v1/user_pb2_grpc.pyi +17 -0
  95. sift/data/__init__.py +0 -0
  96. sift/data/v1/__init__.py +0 -0
  97. sift/data/v1/data_pb2.py +212 -0
  98. sift/data/v1/data_pb2.pyi +745 -0
  99. sift/data/v1/data_pb2_grpc.py +67 -0
  100. sift/data/v1/data_pb2_grpc.pyi +44 -0
  101. sift/ingest/__init__.py +0 -0
  102. sift/ingest/v1/__init__.py +0 -0
  103. sift/ingest/v1/ingest_pb2.py +35 -0
  104. sift/ingest/v1/ingest_pb2.pyi +118 -0
  105. sift/ingest/v1/ingest_pb2_grpc.py +66 -0
  106. sift/ingest/v1/ingest_pb2_grpc.pyi +41 -0
  107. sift/ingestion_configs/__init__.py +0 -0
  108. sift/ingestion_configs/v1/__init__.py +0 -0
  109. sift/ingestion_configs/v1/ingestion_configs_pb2.py +115 -0
  110. sift/ingestion_configs/v1/ingestion_configs_pb2.pyi +332 -0
  111. sift/ingestion_configs/v1/ingestion_configs_pb2_grpc.py +203 -0
  112. sift/ingestion_configs/v1/ingestion_configs_pb2_grpc.pyi +124 -0
  113. sift/notifications/__init__.py +0 -0
  114. sift/notifications/v1/__init__.py +0 -0
  115. sift/notifications/v1/notifications_pb2.py +64 -0
  116. sift/notifications/v1/notifications_pb2.pyi +225 -0
  117. sift/notifications/v1/notifications_pb2_grpc.py +101 -0
  118. sift/notifications/v1/notifications_pb2_grpc.pyi +64 -0
  119. sift/ping/__init__.py +0 -0
  120. sift/ping/v1/__init__.py +0 -0
  121. sift/ping/v1/ping_pb2.py +38 -0
  122. sift/ping/v1/ping_pb2.pyi +36 -0
  123. sift/ping/v1/ping_pb2_grpc.py +66 -0
  124. sift/ping/v1/ping_pb2_grpc.pyi +41 -0
  125. sift/remote_files/__init__.py +0 -0
  126. sift/remote_files/v1/__init__.py +0 -0
  127. sift/remote_files/v1/remote_files_pb2.py +174 -0
  128. sift/remote_files/v1/remote_files_pb2.pyi +472 -0
  129. sift/remote_files/v1/remote_files_pb2_grpc.py +271 -0
  130. sift/remote_files/v1/remote_files_pb2_grpc.pyi +164 -0
  131. sift/report_templates/__init__.py +0 -0
  132. sift/report_templates/v1/__init__.py +0 -0
  133. sift/report_templates/v1/report_templates_pb2.py +146 -0
  134. sift/report_templates/v1/report_templates_pb2.pyi +381 -0
  135. sift/report_templates/v1/report_templates_pb2_grpc.py +169 -0
  136. sift/report_templates/v1/report_templates_pb2_grpc.pyi +104 -0
  137. sift/reports/__init__.py +0 -0
  138. sift/reports/v1/__init__.py +0 -0
  139. sift/reports/v1/reports_pb2.py +193 -0
  140. sift/reports/v1/reports_pb2.pyi +562 -0
  141. sift/reports/v1/reports_pb2_grpc.py +205 -0
  142. sift/reports/v1/reports_pb2_grpc.pyi +136 -0
  143. sift/rule_evaluation/__init__.py +0 -0
  144. sift/rule_evaluation/v1/__init__.py +0 -0
  145. sift/rule_evaluation/v1/rule_evaluation_pb2.py +89 -0
  146. sift/rule_evaluation/v1/rule_evaluation_pb2.pyi +263 -0
  147. sift/rule_evaluation/v1/rule_evaluation_pb2_grpc.py +101 -0
  148. sift/rule_evaluation/v1/rule_evaluation_pb2_grpc.pyi +64 -0
  149. sift/rules/__init__.py +0 -0
  150. sift/rules/v1/__init__.py +0 -0
  151. sift/rules/v1/rules_pb2.py +420 -0
  152. sift/rules/v1/rules_pb2.pyi +1355 -0
  153. sift/rules/v1/rules_pb2_grpc.py +577 -0
  154. sift/rules/v1/rules_pb2_grpc.pyi +351 -0
  155. sift/runs/__init__.py +0 -0
  156. sift/runs/v2/__init__.py +0 -0
  157. sift/runs/v2/runs_pb2.py +150 -0
  158. sift/runs/v2/runs_pb2.pyi +413 -0
  159. sift/runs/v2/runs_pb2_grpc.py +271 -0
  160. sift/runs/v2/runs_pb2_grpc.pyi +164 -0
  161. sift/saved_searches/__init__.py +0 -0
  162. sift/saved_searches/v1/__init__.py +0 -0
  163. sift/saved_searches/v1/saved_searches_pb2.py +144 -0
  164. sift/saved_searches/v1/saved_searches_pb2.pyi +385 -0
  165. sift/saved_searches/v1/saved_searches_pb2_grpc.py +237 -0
  166. sift/saved_searches/v1/saved_searches_pb2_grpc.pyi +144 -0
  167. sift/tags/__init__.py +0 -0
  168. sift/tags/v1/__init__.py +0 -0
  169. sift/tags/v1/tags_pb2.py +49 -0
  170. sift/tags/v1/tags_pb2.pyi +71 -0
  171. sift/tags/v1/tags_pb2_grpc.py +4 -0
  172. sift/tags/v1/tags_pb2_grpc.pyi +17 -0
  173. sift/users/__init__.py +0 -0
  174. sift/users/v2/__init__.py +0 -0
  175. sift/users/v2/users_pb2.py +61 -0
  176. sift/users/v2/users_pb2.pyi +142 -0
  177. sift/users/v2/users_pb2_grpc.py +135 -0
  178. sift/users/v2/users_pb2_grpc.pyi +84 -0
  179. sift/views/__init__.py +0 -0
  180. sift/views/v1/__init__.py +0 -0
  181. sift/views/v1/views_pb2.py +130 -0
  182. sift/views/v1/views_pb2.pyi +466 -0
  183. sift/views/v1/views_pb2_grpc.py +305 -0
  184. sift/views/v1/views_pb2_grpc.pyi +184 -0
  185. sift_grafana/py.typed +0 -0
  186. sift_grafana/sift_query_model.py +64 -0
  187. sift_py/__init__.py +923 -0
  188. sift_py/_internal/__init__.py +5 -0
  189. sift_py/_internal/cel.py +18 -0
  190. sift_py/_internal/channel.py +42 -0
  191. sift_py/_internal/convert/__init__.py +3 -0
  192. sift_py/_internal/convert/json.py +24 -0
  193. sift_py/_internal/convert/protobuf.py +34 -0
  194. sift_py/_internal/convert/timestamp.py +9 -0
  195. sift_py/_internal/test_util/__init__.py +0 -0
  196. sift_py/_internal/test_util/channel.py +136 -0
  197. sift_py/_internal/test_util/fn.py +14 -0
  198. sift_py/_internal/test_util/server_interceptor.py +62 -0
  199. sift_py/_internal/time.py +48 -0
  200. sift_py/_internal/user.py +39 -0
  201. sift_py/data/__init__.py +171 -0
  202. sift_py/data/_channel.py +38 -0
  203. sift_py/data/_deserialize.py +208 -0
  204. sift_py/data/_deserialize_test.py +134 -0
  205. sift_py/data/_service_test.py +276 -0
  206. sift_py/data/_validate.py +10 -0
  207. sift_py/data/error.py +5 -0
  208. sift_py/data/query.py +299 -0
  209. sift_py/data/service.py +497 -0
  210. sift_py/data_import/__init__.py +130 -0
  211. sift_py/data_import/_config.py +167 -0
  212. sift_py/data_import/_config_test.py +166 -0
  213. sift_py/data_import/_csv_test.py +395 -0
  214. sift_py/data_import/_status_test.py +176 -0
  215. sift_py/data_import/_tdms_test.py +238 -0
  216. sift_py/data_import/ch10.py +157 -0
  217. sift_py/data_import/config.py +19 -0
  218. sift_py/data_import/csv.py +259 -0
  219. sift_py/data_import/status.py +113 -0
  220. sift_py/data_import/tdms.py +206 -0
  221. sift_py/data_import/tempfile.py +30 -0
  222. sift_py/data_import/time_format.py +39 -0
  223. sift_py/error.py +11 -0
  224. sift_py/file_attachment/__init__.py +88 -0
  225. sift_py/file_attachment/_internal/__init__.py +0 -0
  226. sift_py/file_attachment/_internal/download.py +13 -0
  227. sift_py/file_attachment/_internal/upload.py +100 -0
  228. sift_py/file_attachment/_service_test.py +161 -0
  229. sift_py/file_attachment/entity.py +30 -0
  230. sift_py/file_attachment/metadata.py +107 -0
  231. sift_py/file_attachment/service.py +142 -0
  232. sift_py/grpc/__init__.py +15 -0
  233. sift_py/grpc/_async_interceptors/__init__.py +0 -0
  234. sift_py/grpc/_async_interceptors/base.py +72 -0
  235. sift_py/grpc/_async_interceptors/metadata.py +36 -0
  236. sift_py/grpc/_interceptors/__init__.py +0 -0
  237. sift_py/grpc/_interceptors/base.py +61 -0
  238. sift_py/grpc/_interceptors/context.py +25 -0
  239. sift_py/grpc/_interceptors/metadata.py +33 -0
  240. sift_py/grpc/_retry.py +70 -0
  241. sift_py/grpc/keepalive.py +34 -0
  242. sift_py/grpc/transport.py +250 -0
  243. sift_py/grpc/transport_test.py +170 -0
  244. sift_py/ingestion/__init__.py +6 -0
  245. sift_py/ingestion/_internal/__init__.py +6 -0
  246. sift_py/ingestion/_internal/channel.py +12 -0
  247. sift_py/ingestion/_internal/error.py +10 -0
  248. sift_py/ingestion/_internal/ingest.py +350 -0
  249. sift_py/ingestion/_internal/ingest_test.py +357 -0
  250. sift_py/ingestion/_internal/ingestion_config.py +130 -0
  251. sift_py/ingestion/_internal/run.py +46 -0
  252. sift_py/ingestion/_service_test.py +478 -0
  253. sift_py/ingestion/buffer.py +189 -0
  254. sift_py/ingestion/channel.py +422 -0
  255. sift_py/ingestion/config/__init__.py +3 -0
  256. sift_py/ingestion/config/telemetry.py +281 -0
  257. sift_py/ingestion/config/telemetry_test.py +405 -0
  258. sift_py/ingestion/config/yaml/__init__.py +0 -0
  259. sift_py/ingestion/config/yaml/error.py +44 -0
  260. sift_py/ingestion/config/yaml/load.py +126 -0
  261. sift_py/ingestion/config/yaml/spec.py +58 -0
  262. sift_py/ingestion/config/yaml/test_load.py +25 -0
  263. sift_py/ingestion/flow.py +73 -0
  264. sift_py/ingestion/manager.py +99 -0
  265. sift_py/ingestion/rule/__init__.py +4 -0
  266. sift_py/ingestion/rule/config.py +11 -0
  267. sift_py/ingestion/service.py +237 -0
  268. sift_py/py.typed +0 -0
  269. sift_py/report_templates/__init__.py +0 -0
  270. sift_py/report_templates/_config_test.py +34 -0
  271. sift_py/report_templates/_service_test.py +94 -0
  272. sift_py/report_templates/config.py +36 -0
  273. sift_py/report_templates/service.py +171 -0
  274. sift_py/rest.py +29 -0
  275. sift_py/rule/__init__.py +0 -0
  276. sift_py/rule/_config_test.py +109 -0
  277. sift_py/rule/_service_test.py +168 -0
  278. sift_py/rule/config.py +229 -0
  279. sift_py/rule/service.py +484 -0
  280. sift_py/yaml/__init__.py +0 -0
  281. sift_py/yaml/_channel_test.py +169 -0
  282. sift_py/yaml/_rule_test.py +207 -0
  283. sift_py/yaml/channel.py +224 -0
  284. sift_py/yaml/report_templates.py +73 -0
  285. sift_py/yaml/rule.py +321 -0
  286. sift_py/yaml/utils.py +15 -0
  287. sift_stack_py-0.3.2.dist-info/LICENSE +7 -0
  288. sift_stack_py-0.3.2.dist-info/METADATA +109 -0
  289. sift_stack_py-0.3.2.dist-info/RECORD +291 -0
  290. sift_stack_py-0.3.2.dist-info/WHEEL +5 -0
  291. sift_stack_py-0.3.2.dist-info/top_level.txt +5 -0
@@ -0,0 +1,44 @@
1
+ from typing import Any, List, Optional
2
+
3
+ from typing_extensions import Self
4
+
5
+ _PREFIX = " "
6
+ _YAML_SEP = "---"
7
+ _LIST_ITEM_PREFIX = "- "
8
+
9
+
10
+ class YamlConfigError(Exception):
11
+ """
12
+ When the YAML config has missing or invalid properties.
13
+ """
14
+
15
+ message: str
16
+
17
+ def __init__(self, message: str):
18
+ super().__init__(message)
19
+
20
+ @classmethod
21
+ def _invalid_property(
22
+ cls,
23
+ actual_value: Any,
24
+ key_name: str,
25
+ expected_type: str,
26
+ ancestors: Optional[List[str]] = None,
27
+ ) -> Self:
28
+ header = f"Expected '{key_name.lstrip(_LIST_ITEM_PREFIX)}' to be <{expected_type}> but it is <{type(actual_value).__name__}>\n{_YAML_SEP}\n"
29
+
30
+ if ancestors is None or len(ancestors) == 0:
31
+ return cls(f"{header}{key_name}: <{expected_type}>")
32
+
33
+ key_value_path = ""
34
+
35
+ current_indentation_level = 0
36
+
37
+ for ancestor in ancestors:
38
+ key_value_path += f"{_PREFIX * current_indentation_level}{ancestor}:\n"
39
+ current_indentation_level += 1
40
+
41
+ key_value_path += f"{_PREFIX * current_indentation_level}{key_name}: <{expected_type}>"
42
+ output = f"{header}{key_value_path}"
43
+
44
+ return cls(output)
@@ -0,0 +1,126 @@
1
+ from pathlib import Path
2
+ from typing import Any, Dict, List, cast
3
+
4
+ import yaml
5
+
6
+ import sift_py.yaml.rule as rule_yaml
7
+ from sift_py.ingestion.config.yaml.error import YamlConfigError
8
+ from sift_py.ingestion.config.yaml.spec import (
9
+ FlowYamlSpec,
10
+ TelemetryConfigYamlSpec,
11
+ )
12
+ from sift_py.yaml.channel import ChannelConfigYamlSpec, _validate_channel, _validate_channel_anchor
13
+ from sift_py.yaml.rule import RuleYamlSpec
14
+ from sift_py.yaml.utils import _type_fqn
15
+
16
+ load_named_expression_modules = rule_yaml.load_named_expression_modules
17
+
18
+
19
+ def read_and_validate(path: Path) -> TelemetryConfigYamlSpec:
20
+ """
21
+ Reads in the telemetry config YAML file found at `path` and validates it. Any errors that may occur at the parsing
22
+ step will return an error whose source is the `yaml` package. Any errors that may occur during the
23
+ validation step will return a `sift_py.ingestion.config.yaml.error.YamlConfigError`.
24
+ """
25
+ raw_config = _read_yaml(path)
26
+ return _validate_yaml(raw_config)
27
+
28
+
29
+ def _validate_yaml(raw_config: Dict[Any, Any]) -> TelemetryConfigYamlSpec:
30
+ asset_name = raw_config.get("asset_name")
31
+
32
+ if not isinstance(asset_name, str):
33
+ raise YamlConfigError._invalid_property(asset_name, "asset_name", "str")
34
+
35
+ ingestion_client_key = raw_config.get("ingestion_client_key")
36
+
37
+ if not isinstance(ingestion_client_key, str):
38
+ raise YamlConfigError._invalid_property(ingestion_client_key, "ingestion_client_key", "str")
39
+
40
+ organization_id = raw_config.get("organization_id")
41
+
42
+ if organization_id is not None and not isinstance(organization_id, str):
43
+ raise YamlConfigError._invalid_property(ingestion_client_key, "organization_id", "str")
44
+
45
+ channels = raw_config.get("channels")
46
+
47
+ if channels is not None:
48
+ if not isinstance(channels, dict):
49
+ raise YamlConfigError._invalid_property(
50
+ channels,
51
+ "channels",
52
+ f"Dict[str, {ChannelConfigYamlSpec}]",
53
+ None,
54
+ )
55
+
56
+ for anchor, channel_config in cast(Dict[Any, Any], channels).items():
57
+ _validate_channel_anchor(anchor)
58
+ _validate_channel(channel_config)
59
+
60
+ rules = raw_config.get("rules")
61
+
62
+ if rules is not None:
63
+ if not isinstance(rules, list):
64
+ raise YamlConfigError._invalid_property(
65
+ rules,
66
+ "rules",
67
+ f"List[{_type_fqn(RuleYamlSpec)}]",
68
+ None,
69
+ )
70
+
71
+ for rule in cast(List[Any], rules):
72
+ rule_yaml._validate_rule(rule)
73
+
74
+ flows = raw_config.get("flows")
75
+
76
+ if flows is not None:
77
+ if not isinstance(flows, list):
78
+ raise YamlConfigError._invalid_property(
79
+ flows,
80
+ "flows",
81
+ f"List[{_type_fqn(FlowYamlSpec)}]",
82
+ None,
83
+ )
84
+
85
+ for flow in cast(List[Any], flows):
86
+ _validate_flow(flow)
87
+
88
+ return cast(TelemetryConfigYamlSpec, raw_config)
89
+
90
+
91
+ def _read_yaml(path: Path) -> Dict[Any, Any]:
92
+ with open(path, "r") as f:
93
+ return cast(Dict[Any, Any], yaml.safe_load(f.read()))
94
+
95
+
96
+ def _validate_flow(val: Any):
97
+ flow = cast(Dict[Any, Any], val)
98
+
99
+ name = flow.get("name")
100
+
101
+ if not isinstance(name, str):
102
+ raise YamlConfigError._invalid_property(
103
+ name,
104
+ "- name",
105
+ "str",
106
+ ["flows"],
107
+ )
108
+
109
+ channels = flow.get("channels")
110
+
111
+ if channels is not None:
112
+ if not isinstance(channels, list):
113
+ raise YamlConfigError._invalid_property(
114
+ channels,
115
+ "channels",
116
+ f"List<{ChannelConfigYamlSpec}>",
117
+ ["flows"],
118
+ )
119
+
120
+ for channel in cast(List[Any], channels):
121
+ try:
122
+ _validate_channel(channel)
123
+ except YamlConfigError as err:
124
+ raise YamlConfigError(
125
+ f"Flow '{name}' contains an invalid channel reference:\n{err}"
126
+ )
@@ -0,0 +1,58 @@
1
+ """
2
+ Formal specification of the types that `sift_py` expects when loading a telemetry config from a YAML file.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Dict, List
8
+
9
+ from typing_extensions import NotRequired, TypedDict
10
+
11
+ import sift_py.yaml.channel as channel_yaml
12
+ import sift_py.yaml.rule as rule_yaml
13
+
14
+ RuleYamlSpec = rule_yaml.RuleYamlSpec
15
+ NamedExpressionYamlSpec = rule_yaml.NamedExpressionYamlSpec
16
+ ChannelConfigYamlSpec = channel_yaml.ChannelConfigYamlSpec
17
+ ChannelEnumTypeYamlSpec = channel_yaml.ChannelEnumTypeYamlSpec
18
+ ChannelBitFieldElementYamlSpec = channel_yaml.ChannelBitFieldElementYamlSpec
19
+
20
+
21
+ class TelemetryConfigYamlSpec(TypedDict):
22
+ """
23
+ Formal spec that defines what the telemetry config should look like in YAML.
24
+
25
+ `asset_name`: The name of the asset to telemeter.
26
+ `ingestion_client_key`: User-defined string-key that uniquely identifies this telemetry config.
27
+ `organization_id`: Optional ID of user's organization. Required if user belongs to multiple orgs.
28
+ `channels`: Sensors that send the data.
29
+ `rules`: Rules that, when evaluated to a true, will perform some sort of acction.
30
+ `flows`: A list of named groups of channels that send data together.
31
+ """
32
+
33
+ asset_name: str
34
+ ingestion_client_key: str
35
+ organization_id: NotRequired[str]
36
+ channels: Dict[str, ChannelConfigYamlSpec]
37
+ rules: NotRequired[List[RuleYamlSpec]]
38
+ flows: NotRequired[List[FlowYamlSpec]]
39
+
40
+
41
+ class FlowYamlSpec(TypedDict):
42
+ """
43
+ Formal spec that defines what a flow should look like in YAML.
44
+ """
45
+
46
+ name: str
47
+ channels: List[ChannelConfigYamlSpec]
48
+
49
+
50
+ class YamlConfigError(Exception):
51
+ """
52
+ When the YAML config has missing or invalid properties.
53
+ """
54
+
55
+ message: str
56
+
57
+ def __init__(self, message: str):
58
+ super().__init__(message)
@@ -0,0 +1,25 @@
1
+ import pytest
2
+
3
+ from sift_py.ingestion.config.yaml import load
4
+ from sift_py.ingestion.config.yaml.error import YamlConfigError
5
+
6
+
7
+ def test__validate_flow():
8
+ load._validate_flow(
9
+ {
10
+ "name": "reading",
11
+ "channels": [
12
+ {"name": "voltage", "data_type": "double"},
13
+ ],
14
+ }
15
+ )
16
+
17
+ with pytest.raises(YamlConfigError):
18
+ load._validate_flow(
19
+ {
20
+ "name": "reading",
21
+ "channels": [
22
+ {"name": "voltage", "data_type": "dubble"},
23
+ ],
24
+ }
25
+ )
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Dict, List, Type, TypedDict
5
+
6
+ from sift.ingest.v1.ingest_pb2 import IngestWithConfigDataChannelValue
7
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import (
8
+ ChannelConfig as ChannelConfigPb,
9
+ )
10
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import (
11
+ FlowConfig as FlowConfigPb,
12
+ )
13
+ from typing_extensions import Self
14
+
15
+ from sift_py._internal.convert.protobuf import AsProtobuf
16
+ from sift_py.ingestion.channel import ChannelConfig, ChannelValue, channel_fqn
17
+
18
+
19
+ class FlowConfig(AsProtobuf):
20
+ """
21
+ Describes a flow which is a set of channels whose values are often ingested together, allowing
22
+ users to send multiple data points for multiple channels in a single request.
23
+
24
+ `channel_by_fqn`:
25
+ A mapping of a channel's fully-qualified name to the index of the `sift_py.ingestion.channel.ChannelConfig`
26
+ as it appears in the `channels` attribute.
27
+ """
28
+
29
+ name: str
30
+ channels: List[ChannelConfig]
31
+ channel_by_fqn: Dict[str, int]
32
+
33
+ def __init__(self, name: str, channels: List[ChannelConfig]):
34
+ self.name = name
35
+ self.channels = channels
36
+ self.channel_by_fqn = {channel_fqn(c): i for i, c in enumerate(channels)}
37
+
38
+ def as_pb(self, klass: Type[FlowConfigPb]) -> FlowConfigPb:
39
+ return klass(
40
+ name=self.name,
41
+ channels=[conf.as_pb(ChannelConfigPb) for conf in self.channels],
42
+ )
43
+
44
+ @classmethod
45
+ def from_pb(cls, message: FlowConfigPb) -> Self:
46
+ return cls(
47
+ name=message.name,
48
+ channels=[ChannelConfig.from_pb(c) for c in message.channels],
49
+ )
50
+
51
+
52
+ class Flow(TypedDict):
53
+ """
54
+ Represents a single flow that will be sent to Sift. Because this class uses `sift_py.ingestion.channel.ChannelValue`
55
+ which is a fully qualified channel value, a specific ordering of items in `channel_values` is not required. If a
56
+ particular flow has 5 channels, it is okay to send only data for 3 channels using this class.
57
+ """
58
+
59
+ flow_name: str
60
+ timestamp: datetime
61
+ channel_values: List[ChannelValue]
62
+
63
+
64
+ class FlowOrderedChannelValues(TypedDict):
65
+ """
66
+ Represents a single flow that will be sent to Sift. Unlike `sift_py.ingestion.flow.Flow`, this class requires
67
+ that the ordering of channel values in `channel_values` match what the flow associated with `flow_name` expects.
68
+ If a channel doesn't have particular data to send for a particular time, `sift_py.ingestion.channel.empty_value` should be used
69
+ """
70
+
71
+ flow_name: str
72
+ timestamp: datetime
73
+ channel_values: List[IngestWithConfigDataChannelValue]
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from typing import Callable, Dict, Iterator, Optional, TypedDict
5
+
6
+ from typing_extensions import Self, TypeAlias
7
+
8
+ from sift_py.grpc.transport import SiftChannel
9
+ from sift_py.ingestion.config.telemetry import TelemetryConfig
10
+ from sift_py.ingestion.service import IngestionService
11
+
12
+ IngestionServiceBuilder: TypeAlias = Callable[[SiftChannel], IngestionService]
13
+
14
+
15
+ class IngestionServicesManager:
16
+ """
17
+ Allows for the initialization of multiple instances of `sift_py.ingestion.service.IngestionService` from
18
+ either telemetry configs or builders under a single wrapper class that assists in managing data-ingestion
19
+ for multiple telemetry configs.
20
+
21
+ The initializer of this class can be used directly, but prefer to use either `from_builders` or `from_telemetry_configs`.
22
+ Prefer to use `from_builders` if you have custom options that you want to provide to `sift_py.ingestion.service.IngestionService.__init__`.
23
+
24
+ Example usage:
25
+
26
+ ```python
27
+ manager = IngestionServicesManager.from_telementry_configs(grpc_channel, {
28
+ "config_a": config_a,
29
+ "config_b": config_b,
30
+ })
31
+
32
+ with manager.ingestion_service("config_a") as config_a:
33
+ config_a.try_ingest_flow(...)
34
+
35
+ with manager.ingestion_service("config_b") as config_b:
36
+ config_b.try_ingest_flow(...)
37
+ ```
38
+ """
39
+
40
+ _transport_channel: SiftChannel
41
+ _ingestion_services: Dict[str, IngestionService]
42
+
43
+ def __init__(
44
+ self, transport_channel: SiftChannel, ingestion_services: Dict[str, IngestionService]
45
+ ):
46
+ self._transport_channel = transport_channel
47
+ self._ingestion_services = ingestion_services
48
+
49
+ @classmethod
50
+ def from_builders(
51
+ cls, channel: SiftChannel, builders: Dict[str, IngestionServiceBuilder]
52
+ ) -> Self:
53
+ return cls(
54
+ transport_channel=channel,
55
+ ingestion_services={key: builder(channel) for key, builder in builders.items()},
56
+ )
57
+
58
+ @classmethod
59
+ def from_telemetry_configs(
60
+ cls, channel: SiftChannel, telemetry_configs: Dict[str, TelemetryConfig]
61
+ ) -> Self:
62
+ return cls(
63
+ transport_channel=channel,
64
+ ingestion_services={
65
+ key: IngestionService(channel, config) for key, config in telemetry_configs.items()
66
+ },
67
+ )
68
+
69
+ def get_ingestion_service_by_identifier(self, identifier: str) -> Optional[IngestionService]:
70
+ return self._ingestion_services.get(identifier)
71
+
72
+ def __getitem__(self, identifier: str) -> Optional[IngestionService]:
73
+ return self.get_ingestion_service_by_identifier(identifier)
74
+
75
+ @contextmanager
76
+ def ingestion_service(self, identifier: str) -> Iterator[IngestionService]:
77
+ ingestion_service = self[identifier]
78
+
79
+ if ingestion_service is None:
80
+ raise IngestionServiceManagerError(
81
+ f"An ingestion service is not configured for the identifier '{identifier}'."
82
+ )
83
+
84
+ yield ingestion_service
85
+
86
+
87
+ class IngestionServiceManagerError(Exception):
88
+ def __init__(self, msg: str):
89
+ return super().__init__(msg)
90
+
91
+
92
+ class TelemetryConfigByIdentifierMap(TypedDict):
93
+ identifier: str
94
+ telemetry_config: TelemetryConfig
95
+
96
+
97
+ class IngestionConfigServiceBuilderIdentifierMap(TypedDict):
98
+ identifier: str
99
+ builder: IngestionServiceBuilder
@@ -0,0 +1,4 @@
1
+ """
2
+ This module is deprecated and exists only to re-export members
3
+ of `sift_py.rule`. Prefer to use that module.
4
+ """
@@ -0,0 +1,11 @@
1
+ import sift_py.rule.config as rule_config
2
+
3
+ RuleConfig = rule_config.RuleConfig
4
+ RuleAction = rule_config.RuleAction
5
+ RuleActionCreateDataReviewAnnotation = rule_config.RuleActionCreateDataReviewAnnotation
6
+ RuleActionCreatePhaseAnnotation = rule_config.RuleActionCreatePhaseAnnotation
7
+ RuleActionAnnotationKind = rule_config.RuleActionAnnotationKind
8
+ RuleActionKind = rule_config.RuleActionKind
9
+ RuleActionKindStrRep = rule_config.RuleActionKindStrRep
10
+ ExpressionChannelReference = rule_config.ExpressionChannelReference
11
+ ExpressionChannelReferenceChannelConfig = rule_config.ExpressionChannelReferenceChannelConfig
@@ -0,0 +1,237 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Dict, List, Optional
5
+
6
+ from sift.ingest.v1.ingest_pb2 import (
7
+ IngestWithConfigDataChannelValue,
8
+ IngestWithConfigDataStreamRequest,
9
+ )
10
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import IngestionConfig
11
+
12
+ from sift_py.grpc.transport import SiftChannel
13
+ from sift_py.ingestion._internal.ingest import _IngestionServiceImpl
14
+ from sift_py.ingestion.buffer import BufferedIngestionService, OnErrorCallback
15
+ from sift_py.ingestion.channel import ChannelValue
16
+ from sift_py.ingestion.config.telemetry import TelemetryConfig
17
+ from sift_py.ingestion.flow import Flow, FlowConfig, FlowOrderedChannelValues
18
+
19
+
20
+ class IngestionService(_IngestionServiceImpl):
21
+ """
22
+ A fully configured service that, when instantiated, is ready to start ingesting data.
23
+
24
+ - `transport_channel`: A gRPC transport channel. Prefer to use `SiftChannel`.
25
+ - `ingestion_config`: The underlying strongly-typed ingestion config. Users of this service don't need to be concerned with this.
26
+ - `asset_name`: The name of the asset to telemeter.
27
+ - `flow_configs_by_name`: A mapping of flow config name to the actual flow config.
28
+ - `run_id`: The ID of the optional run to associated ingested data with.
29
+ - `organization_id`: ID of the organization of the user.
30
+ - `overwrite_rules`:
31
+ If there are rules in Sift that aren't found in the local telemetry config, then initializing
32
+ an `IngestionService` will raise an exception advising the user to update their telemetry config
33
+ with the missing rule before proceeding. Setting this field to `True` replace all rules currently
34
+ in Sift with the rules in the telemetry config.
35
+ - `end_stream_on_error`:
36
+ By default any errors that may occur during ingestion API-side are produced asynchronously and ingestion
37
+ won't be interrupted. The errors produced are surfaced on the user errors page. Setting this field to `True`
38
+ will ensure that any errors that occur during ingestion is returned immediately, terminating the stream. This
39
+ is useful for debugging purposes.
40
+ """
41
+
42
+ transport_channel: SiftChannel
43
+ ingestion_config: IngestionConfig
44
+ asset_name: str
45
+ flow_configs_by_name: Dict[str, FlowConfig]
46
+ run_id: Optional[str]
47
+ organization_id: Optional[str]
48
+ end_stream_on_error: bool
49
+
50
+ def __init__(
51
+ self,
52
+ channel: SiftChannel,
53
+ config: TelemetryConfig,
54
+ run_id: Optional[str] = None,
55
+ overwrite_rules: bool = False,
56
+ end_stream_on_error: bool = False,
57
+ ):
58
+ super().__init__(channel, config, run_id, overwrite_rules, end_stream_on_error)
59
+
60
+ def ingest(self, *requests: IngestWithConfigDataStreamRequest):
61
+ """
62
+ This method performs the actual data ingestion given a list of data ingestion requests.
63
+ """
64
+ super().ingest(*requests)
65
+
66
+ def attach_run(
67
+ self,
68
+ channel: SiftChannel,
69
+ run_name: str,
70
+ description: Optional[str] = None,
71
+ organization_id: Optional[str] = None,
72
+ tags: Optional[List[str]] = None,
73
+ ):
74
+ """
75
+ Retrieve an existing run or create one to use during this period of ingestion.
76
+ """
77
+ super().attach_run(channel, run_name, description, organization_id, tags)
78
+
79
+ def detach_run(self):
80
+ """
81
+ Detach run from this period of ingestion. Subsequent data ingested won't be associated with
82
+ the run being detached.
83
+ """
84
+ super().detach_run()
85
+
86
+ def try_create_ingestion_request(
87
+ self,
88
+ flow_name: str,
89
+ timestamp: datetime,
90
+ channel_values: List[ChannelValue],
91
+ ) -> IngestWithConfigDataStreamRequest:
92
+ """
93
+ Creates an `IngestWithConfigDataStreamRequest`, i.e. a flow, given a `flow_name` and a
94
+ list of `ChannelValue` objects. Channels that appear in the flow config but not in the
95
+ `channel_values` will be assigned an empty value.
96
+
97
+ This function will perform validation checks to ensure that the values provided in the dictionary; this
98
+ includes:
99
+ - Making sure the flow exists
100
+ - Making sure that the there are no unexpected channels provided for the given flow
101
+ - Making sure the channel value is the expected type
102
+ - Making sure that the timestamp is in UTC
103
+ - Making sure channels that belong to a component have the 'component' field for the channel value
104
+
105
+ If any of the above validations fail then a `IngestionValidationError` will be raised.
106
+
107
+ If for performance reasons you'd prefer to skip the validation checks, or perhaps you did the
108
+ validations on your own, prefer to use `create_ingestion_request`. Any errors that occur during
109
+ ingestion will be handled by the Sift API.
110
+ """
111
+ return super().try_create_ingestion_request(flow_name, timestamp, channel_values)
112
+
113
+ def create_ingestion_request(
114
+ self,
115
+ flow_name: str,
116
+ timestamp: datetime,
117
+ channel_values: List[IngestWithConfigDataChannelValue],
118
+ ) -> IngestWithConfigDataStreamRequest:
119
+ """
120
+ Unlike `try_create_ingestion_request`, this skips argument validations. Useful for when user has already done their own
121
+ argument validation or if they require low-latency execution time client-side.
122
+
123
+ If there are errors that occur during ingestion and the `end_stream_on_error` attribute is set to `False`,
124
+ the data ingestion stream will skip over them and errors instead will be produced asynchronously and become
125
+ available in the UI application in the errors page. If `end_stream_on_error` is set to `True`, then the
126
+ data ingestion stream will be terminated if an error is encountered during ingestion.
127
+
128
+ These are some things to look out for when using this method instead of `try_create_ingestion_request`:
129
+ - Values in `channel_values` must appear in the same order its corresponding channel appears in the flow config
130
+ associated with the `flow_name`.
131
+ - The length of `channel_values` is expected to match the length of the channel configs list of the flow config
132
+ associated with `flow_name`. `sift_py.ingestion.channel.empty_value()` may be used if you require empty values.
133
+ - The `timestamp` must be in UTC.
134
+ """
135
+ return super().create_ingestion_request(flow_name, timestamp, channel_values)
136
+
137
+ def ingest_flows(self, *flows: FlowOrderedChannelValues):
138
+ """
139
+ Combines the requests creation step and ingestion into a single call.
140
+ See `create_ingestion_request` for information about how client-side validations are handled.
141
+ """
142
+ return super().ingest_flows(*flows)
143
+
144
+ def try_ingest_flows(self, *flows: Flow):
145
+ """
146
+ Combines the requests creation step and ingestion into a single call.
147
+ See `try_create_ingestion_request` for information about how client-side validations are handled.
148
+ """
149
+ return super().try_ingest_flows(*flows)
150
+
151
+ def buffered_ingestion(
152
+ self,
153
+ buffer_size: Optional[int] = None,
154
+ flush_interval_sec: Optional[float] = None,
155
+ on_error: Optional[OnErrorCallback] = None,
156
+ ) -> BufferedIngestionService:
157
+ """
158
+ This method automates buffering requests and streams them in batches. It is recommended to be used
159
+ in a with-block. Failure to put this in a with-block may result in some data not being ingested unless
160
+ the caller explicitly calls `sift_py.ingestion.buffer.BufferedIngestionService.flush` before the returned
161
+ instance of `sift_py.ingestion.buffer.BufferedIngestionService` goes out of scope. Once the with-block
162
+ is exited then a final call to the aforementioned `flush` method will be made to ingest the remaining data.
163
+
164
+ Buffered ingestion works by automatically flushing and ingesting data into Sift whenever the buffer is filled.
165
+ The size of the buffer is configured via the `buffer_size` argument and defaults to `sift_py.ingestion.buffer.DEFAULT_BUFFER_SIZE`.
166
+
167
+ It is also possible to configure buffered ingestion to periodically flush the buffer regardless of whether or not the buffer
168
+ is filled. The interval between flushes is set via the `flush_interval_sec` argument which is the number of seconds between each flush.
169
+ If a flush were to occur due to the buffer being filled, then the timer will restart. If `flush_interval_sec` is `None`, then flushes will only
170
+ occur once the buffer is filled and at the end of the scope of the with-block.
171
+
172
+ If an error were to occur that would cause the context manager to call `__exit__`, one last attempt to flush the buffer will be made
173
+ before the error is re-raised for the caller to handle. If the caller would instead like to customize `__exit__` behavior in the case
174
+ of an error, they can make use of the `on_error` argument whose type signature is a function where the first argument is the error,
175
+ the second is the buffer containing the uningested request, and the third argument being a function where, when called, will attempt
176
+ to flush the buffer.
177
+
178
+ Example usage:
179
+
180
+ ```python
181
+ # With client-side validations
182
+ with ingestion_service.buffered_ingestion() as buffered_ingestion:
183
+ for _ in range(10_000):
184
+ buffered_ingestion.try_ingest_flows({
185
+ "flow_name": "readings",
186
+ "timestamp": datetime.now(timezone.utc),
187
+ "channel_values": [
188
+ {
189
+ "channel_name": "my-channel",
190
+ ],
191
+ })
192
+
193
+ # Without client-side validations and a custom buffer size
194
+ with ingestion_service.buffered_ingestion(2_000) as buffered_ingestion:
195
+ for _ in range(6_000):
196
+ buffered_ingestion.ingest_flows({
197
+ "flow_name": "readings",
198
+ "timestamp": datetime.now(timezone.utc),
199
+ "channel_values": [double_value(3)]
200
+ })
201
+
202
+ # With default buffer size and periodic flushes of 3.2 seconds
203
+ with ingestion_service.buffered_ingestion(flush_interval_sec=3.2) as buffered_ingestion:
204
+ for _ in range(6_000):
205
+ buffered_ingestion.ingest_flows({
206
+ "flow_name": "readings",
207
+ "timestamp": datetime.now(timezone.utc),
208
+ "channel_values": [double_value(3)]
209
+ })
210
+
211
+ # Custom code to run when error
212
+ def on_error_calback(err, buffer, flush):
213
+ # Save contents of buffer to disk
214
+ ...
215
+ # Try once more to flush the buffer
216
+ flush()
217
+
218
+ with ingestion_service.buffered_ingestion(on_error=on_error_calback) as buffered_ingestion:
219
+ ...
220
+ ```
221
+ """
222
+ return BufferedIngestionService(self, buffer_size, flush_interval_sec, on_error)
223
+
224
+ def create_flow(self, flow_config: FlowConfig):
225
+ """
226
+ Like `try_create_new_flow` but will automatically overwrite any existing flow config with `flow_config` if they
227
+ share the same name. If you'd an exception to be raise in the case of a name collision then see `try_create_new_flow`.
228
+ """
229
+ super().create_flow(flow_config)
230
+
231
+ def try_create_flow(self, flow_config: FlowConfig):
232
+ """
233
+ Tries to create a new flow at runtime. Will raise an `IngestionValidationError` if there already exists
234
+ a flow with the name of the `flow_config` argument. If you'd like to overwrite any flow configs with that
235
+ have the same name as the provided `flow_config`, then see `create_new_flow`.
236
+ """
237
+ super().try_create_flow(flow_config)
sift_py/py.typed ADDED
File without changes
File without changes