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,281 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List, Optional, cast
5
+
6
+ from typing_extensions import Self
7
+
8
+ from sift_py._internal.channel import channel_fqn
9
+ from sift_py.ingestion.channel import (
10
+ ChannelBitFieldElement,
11
+ ChannelConfig,
12
+ ChannelDataType,
13
+ ChannelEnumType,
14
+ )
15
+ from sift_py.ingestion.config.yaml.load import (
16
+ load_named_expression_modules,
17
+ read_and_validate,
18
+ )
19
+ from sift_py.ingestion.config.yaml.spec import TelemetryConfigYamlSpec
20
+ from sift_py.ingestion.flow import FlowConfig
21
+ from sift_py.rule.config import (
22
+ ExpressionChannelReference,
23
+ ExpressionChannelReferenceChannelConfig,
24
+ RuleAction,
25
+ RuleActionAnnotationKind,
26
+ RuleActionCreateDataReviewAnnotation,
27
+ RuleActionCreatePhaseAnnotation,
28
+ RuleConfig,
29
+ )
30
+ from sift_py.yaml.rule import RuleYamlSpec, load_rule_modules
31
+
32
+
33
+ class TelemetryConfig:
34
+ """
35
+ Configurations necessary to start ingestion.
36
+ - `asset_name`: The name of the asset that you wish to telemeter data for.
37
+ - `ingestion_client_key`: An arbitrary string chosen by the user to uniquely identify this ingestion configuration.
38
+ - `flows`: A single flow can specify a single channel value or a set of channel values that are ingested together.
39
+ - `organization_id`: ID of your organization in Sift. This field is only required if your user belongs to multiple organizations.
40
+ - `rules`: Rules to evaluate during ingestion.
41
+ """
42
+
43
+ asset_name: str
44
+ ingestion_client_key: str
45
+ organization_id: Optional[str]
46
+ flows: List[FlowConfig]
47
+ rules: List[RuleConfig]
48
+
49
+ def __init__(
50
+ self,
51
+ asset_name: str,
52
+ ingestion_client_key: str,
53
+ organization_id: Optional[str] = None,
54
+ flows: List[FlowConfig] = [],
55
+ rules: List[RuleConfig] = [],
56
+ ):
57
+ """
58
+ Will raise a `TelemetryConfigValidationError` under the following conditions:
59
+ - Multiple flows with the same name
60
+ - Multiple rules with the same name
61
+ - Identical channels in the same flow
62
+ """
63
+ self.__class__.validate_flows(flows)
64
+ self.__class__.validate_rules(rules)
65
+
66
+ self.asset_name = asset_name
67
+ self.ingestion_client_key = ingestion_client_key
68
+ self.organization_id = organization_id
69
+ self.flows = flows
70
+ self.rules = rules
71
+
72
+ @staticmethod
73
+ def validate_rules(rules: List[RuleConfig]):
74
+ """
75
+ Ensure that there are no rules with identical names
76
+ """
77
+ seen_rule_names = set()
78
+
79
+ for rule in rules:
80
+ if rule.name in seen_rule_names:
81
+ raise TelemetryConfigValidationError(
82
+ f"Can't have two rules with identical names, '{rule.name}'."
83
+ )
84
+ seen_rule_names.add(rule.name)
85
+
86
+ @staticmethod
87
+ def validate_flows(flows: List[FlowConfig]):
88
+ """
89
+ Ensures no duplicate channels and flows with the same name, otherwise raises a `TelemetryConfigValidationError` exception.
90
+ """
91
+ flow_names = set()
92
+
93
+ for flow in flows:
94
+ seen_channels = set()
95
+
96
+ if flow.name in flow_names:
97
+ raise TelemetryConfigValidationError(
98
+ f"Can't have two flows with the same name, '{flow.name}'."
99
+ )
100
+
101
+ flow_names.add(flow.name)
102
+
103
+ for channel in flow.channels:
104
+ fqn = channel.fqn()
105
+
106
+ if fqn in seen_channels:
107
+ raise TelemetryConfigValidationError(
108
+ f"Can't have two identical channels, '{fqn}', in flow '{flow.name}'."
109
+ )
110
+ else:
111
+ seen_channels.add(fqn)
112
+
113
+ @classmethod
114
+ def try_from_yaml(
115
+ cls,
116
+ path: Path,
117
+ named_expression_modules: Optional[List[Path]] = None,
118
+ named_rule_modules: Optional[List[Path]] = None,
119
+ ) -> Self:
120
+ """
121
+ Initializes a telemetry config from a YAML file found at the provided `path` as well as optional
122
+ paths to named expression modules if named expressions are leveraged.
123
+ """
124
+
125
+ config_as_yaml = read_and_validate(path)
126
+
127
+ named_expressions = {}
128
+ rule_modules = []
129
+ if named_expression_modules is not None:
130
+ named_expressions = load_named_expression_modules(named_expression_modules)
131
+ if named_rule_modules is not None:
132
+ rule_modules = load_rule_modules(named_rule_modules)
133
+
134
+ return cls._from_yaml(config_as_yaml, named_expressions, rule_modules)
135
+
136
+ @classmethod
137
+ def _from_yaml(
138
+ cls,
139
+ config_as_yaml: TelemetryConfigYamlSpec,
140
+ named_expressions: Dict[str, str] = {},
141
+ rule_modules: List[RuleYamlSpec] = [],
142
+ ) -> Self:
143
+ rules = []
144
+ flows = []
145
+
146
+ for flow in config_as_yaml.get("flows", []):
147
+ channels = []
148
+
149
+ for channel in flow["channels"]:
150
+ data_type = cast(ChannelDataType, ChannelDataType.from_str(channel["data_type"]))
151
+
152
+ bit_field_elements = []
153
+ for bit_field_element in channel.get("bit_field_elements", []):
154
+ bit_field_elements.append(
155
+ ChannelBitFieldElement(
156
+ name=bit_field_element["name"],
157
+ index=bit_field_element["index"],
158
+ bit_count=bit_field_element["bit_count"],
159
+ )
160
+ )
161
+
162
+ enum_types = []
163
+ for enum_type in channel.get("enum_types", []):
164
+ enum_types.append(
165
+ ChannelEnumType(
166
+ name=enum_type["name"],
167
+ key=enum_type["key"],
168
+ )
169
+ )
170
+
171
+ channels.append(
172
+ ChannelConfig(
173
+ name=channel["name"],
174
+ data_type=data_type,
175
+ description=channel.get("description"),
176
+ unit=channel.get("unit"),
177
+ component=channel.get("component"),
178
+ bit_field_elements=bit_field_elements,
179
+ enum_types=enum_types,
180
+ )
181
+ )
182
+
183
+ flows.append(
184
+ FlowConfig(
185
+ name=flow["name"],
186
+ channels=channels,
187
+ )
188
+ )
189
+
190
+ yaml_rules = config_as_yaml.get("rules", []) + rule_modules
191
+
192
+ for rule in yaml_rules:
193
+ action: Optional[RuleAction] = None
194
+ description: str = ""
195
+ annotation_type = RuleActionAnnotationKind.from_str(rule["type"])
196
+ tags = rule.get("tags")
197
+ description = rule.get("description", "")
198
+
199
+ action = RuleActionCreatePhaseAnnotation(tags)
200
+ if annotation_type == RuleActionAnnotationKind.REVIEW:
201
+ action = RuleActionCreateDataReviewAnnotation(
202
+ assignee=rule.get("assignee"),
203
+ tags=tags,
204
+ )
205
+
206
+ channel_references: List[
207
+ ExpressionChannelReference | ExpressionChannelReferenceChannelConfig
208
+ ] = []
209
+
210
+ for channel_reference in rule.get("channel_references", []):
211
+ for ref, val in channel_reference.items():
212
+ name = val["name"]
213
+ component = val.get("component")
214
+
215
+ channel_references.append(
216
+ {
217
+ "channel_reference": ref,
218
+ "channel_identifier": channel_fqn(name, component),
219
+ }
220
+ )
221
+
222
+ expression = rule.get("expression", "")
223
+ rule_client_key = rule.get("rule_client_key", "")
224
+ if isinstance(expression, str):
225
+ rules.append(
226
+ RuleConfig(
227
+ name=rule["name"],
228
+ description=description,
229
+ expression=expression,
230
+ action=action,
231
+ rule_client_key=rule_client_key,
232
+ channel_references=channel_references,
233
+ )
234
+ )
235
+ else:
236
+ expression_name = cast(str, expression.get("name"))
237
+
238
+ expr = named_expressions.get(expression_name)
239
+
240
+ if expr is None:
241
+ raise TelemetryConfigValidationError(
242
+ f"Named expression '{expression_name}' could not be found. Make sure it was loaded in."
243
+ )
244
+
245
+ sub_expressions = rule.get("sub_expressions", [])
246
+
247
+ sub_exprs: Dict[str, Any] = {}
248
+ for sub_expression in sub_expressions:
249
+ for iden, value in sub_expression.items():
250
+ sub_exprs[iden] = value
251
+
252
+ rules.append(
253
+ RuleConfig(
254
+ name=rule["name"],
255
+ description=description,
256
+ expression=expr,
257
+ action=action,
258
+ rule_client_key=rule_client_key,
259
+ channel_references=channel_references,
260
+ sub_expressions=sub_exprs,
261
+ )
262
+ )
263
+
264
+ return cls(
265
+ asset_name=config_as_yaml["asset_name"],
266
+ ingestion_client_key=config_as_yaml["ingestion_client_key"],
267
+ organization_id=config_as_yaml.get("organization_id"),
268
+ rules=rules,
269
+ flows=flows,
270
+ )
271
+
272
+
273
+ class TelemetryConfigValidationError(Exception):
274
+ """
275
+ When the telemetry config has invalid properties
276
+ """
277
+
278
+ message: str
279
+
280
+ def __init__(self, message: str):
281
+ super().__init__(message)
@@ -0,0 +1,405 @@
1
+ from pathlib import Path
2
+ from typing import Any, Dict, cast
3
+
4
+ import pytest
5
+ import yaml
6
+ from pytest_mock import MockerFixture, MockFixture
7
+
8
+ import sift_py.ingestion.config.telemetry
9
+ import sift_py.ingestion.config.yaml.load
10
+ from sift_py._internal.test_util.fn import _mock_path as _mock_path_imp
11
+ from sift_py.ingestion.channel import ChannelConfig, ChannelDataType
12
+ from sift_py.ingestion.config.telemetry import TelemetryConfig, TelemetryConfigValidationError
13
+ from sift_py.ingestion.config.yaml.load import (
14
+ _validate_yaml,
15
+ load_named_expression_modules,
16
+ read_and_validate,
17
+ )
18
+ from sift_py.ingestion.flow import FlowConfig
19
+ from sift_py.ingestion.rule.config import (
20
+ RuleActionCreateDataReviewAnnotation,
21
+ RuleActionCreatePhaseAnnotation,
22
+ RuleActionKind,
23
+ RuleConfig,
24
+ )
25
+
26
+ _mock_path = _mock_path_imp(sift_py.ingestion.config.telemetry)
27
+
28
+
29
+ def test_telemetry_config_load_from_yaml(mocker: MockFixture):
30
+ raw_yaml_config = cast(Dict[Any, Any], yaml.safe_load(TEST_YAML_CONFIG_STR))
31
+ yaml_config = _validate_yaml(raw_yaml_config)
32
+
33
+ mock_read_and_validate = mocker.patch(_mock_path(read_and_validate))
34
+ mock_read_and_validate.return_value = yaml_config
35
+
36
+ mock_load_named_expression_modules = mocker.patch(_mock_path(load_named_expression_modules))
37
+ mock_load_named_expression_modules.return_value = {
38
+ "log_substring_contains": "contains($1, $substr)",
39
+ "kinetic_energy_gt": "0.5 * $mass * $1 * $1 > $threshold",
40
+ }
41
+
42
+ dummy_yaml_path = Path()
43
+ dummy_named_expr_mod_path = Path()
44
+
45
+ telemetry_config = TelemetryConfig.try_from_yaml(dummy_yaml_path, [dummy_named_expr_mod_path])
46
+
47
+ assert telemetry_config.asset_name == "LunarVehicle426"
48
+ assert telemetry_config.ingestion_client_key == "lunar_vehicle_426"
49
+ assert len(telemetry_config.flows) == 3
50
+
51
+ flow_configs = telemetry_config.flows
52
+ assert flow_configs[0].name == "readings"
53
+ assert flow_configs[1].name == "partial_readings"
54
+ assert flow_configs[2].name == "logs"
55
+
56
+ readings_flow, partial_readings_flow, logs_flow = flow_configs
57
+ assert len(readings_flow.channels) == 4
58
+ assert len(partial_readings_flow.channels) == 2
59
+ assert len(logs_flow.channels) == 1
60
+
61
+ log_channel = logs_flow.channels[0]
62
+ assert log_channel.name == "log"
63
+ assert log_channel.description == "asset logs"
64
+ assert log_channel.data_type == ChannelDataType.STRING
65
+
66
+ velocity_channel, voltage_channel, vehicle_state_channel, gpio_channel = readings_flow.channels
67
+ assert velocity_channel.name == "velocity"
68
+ assert velocity_channel.data_type == ChannelDataType.DOUBLE
69
+ assert velocity_channel.unit == "Miles Per Hour"
70
+ assert velocity_channel.component == "mainmotor"
71
+ assert velocity_channel.description == "speed"
72
+
73
+ assert voltage_channel.name == "voltage"
74
+ assert voltage_channel.data_type == ChannelDataType.INT_32
75
+ assert voltage_channel.unit == "Volts"
76
+ assert voltage_channel.description == "voltage at the source"
77
+
78
+ assert vehicle_state_channel.name == "vehicle_state"
79
+ assert vehicle_state_channel.data_type == ChannelDataType.ENUM
80
+ assert vehicle_state_channel.unit == "vehicle state"
81
+ assert vehicle_state_channel.description == "vehicle state"
82
+ assert len(vehicle_state_channel.enum_types) == 3
83
+ assert vehicle_state_channel.enum_types[0].name == "Accelerating"
84
+ assert vehicle_state_channel.enum_types[0].key == 0
85
+ assert vehicle_state_channel.enum_types[1].name == "Decelerating"
86
+ assert vehicle_state_channel.enum_types[1].key == 1
87
+ assert vehicle_state_channel.enum_types[2].name == "Stopped"
88
+ assert vehicle_state_channel.enum_types[2].key == 2
89
+
90
+ assert gpio_channel.name == "gpio"
91
+ assert gpio_channel.data_type == ChannelDataType.BIT_FIELD
92
+ assert gpio_channel.unit is None
93
+ assert gpio_channel.description == "on/off values for pins on gpio"
94
+ assert len(gpio_channel.bit_field_elements) == 4
95
+ assert gpio_channel.bit_field_elements[0].name == "12v"
96
+ assert gpio_channel.bit_field_elements[0].index == 0
97
+ assert gpio_channel.bit_field_elements[0].bit_count == 1
98
+ assert gpio_channel.bit_field_elements[1].name == "charge"
99
+ assert gpio_channel.bit_field_elements[1].index == 1
100
+ assert gpio_channel.bit_field_elements[1].bit_count == 2
101
+ assert gpio_channel.bit_field_elements[2].name == "led"
102
+ assert gpio_channel.bit_field_elements[2].index == 3
103
+ assert gpio_channel.bit_field_elements[2].bit_count == 4
104
+ assert gpio_channel.bit_field_elements[3].name == "heater"
105
+ assert gpio_channel.bit_field_elements[3].index == 7
106
+ assert gpio_channel.bit_field_elements[3].bit_count == 1
107
+
108
+ assert len(telemetry_config.rules) == 4
109
+
110
+ (
111
+ overheating_rule,
112
+ speeding_rule,
113
+ failures_rule,
114
+ kinetic_energy_rule,
115
+ ) = telemetry_config.rules
116
+
117
+ assert overheating_rule.name == "overheating"
118
+ assert overheating_rule.description == "Checks for vehicle overheating"
119
+ assert overheating_rule.expression == '$1 == "Accelerating" && $2 > 80'
120
+ assert overheating_rule.action.kind() == RuleActionKind.ANNOTATION # type: ignore
121
+ assert isinstance(overheating_rule.action, RuleActionCreateDataReviewAnnotation)
122
+
123
+ assert speeding_rule.name == "speeding"
124
+ assert speeding_rule.description == "Checks high vehicle speed"
125
+ assert speeding_rule.expression == "$1 > 20"
126
+ assert overheating_rule.action.kind() == RuleActionKind.ANNOTATION
127
+ assert isinstance(speeding_rule.action, RuleActionCreatePhaseAnnotation)
128
+
129
+ assert failures_rule.name == "failures"
130
+ assert failures_rule.description == "Checks for failure logs"
131
+ assert failures_rule.expression == 'contains($1, "ERROR")'
132
+ assert overheating_rule.action.kind() == RuleActionKind.ANNOTATION
133
+ assert isinstance(failures_rule.action, RuleActionCreateDataReviewAnnotation)
134
+
135
+ assert kinetic_energy_rule.name == "kinetic_energy"
136
+ assert kinetic_energy_rule.description == "Tracks high energy output while in motion"
137
+ assert kinetic_energy_rule.expression == "0.5 * 10 * $1 * $1 > 470"
138
+ assert overheating_rule.action.kind() == RuleActionKind.ANNOTATION
139
+ assert isinstance(kinetic_energy_rule.action, RuleActionCreateDataReviewAnnotation)
140
+
141
+
142
+ def test_telemetry_config_err_if_duplicate_channels_in_flow(mocker: MockerFixture):
143
+ """
144
+ Raise an error if there are duplicate channels in a flow.
145
+ """
146
+ raw_yaml_config = cast(
147
+ Dict[Any, Any], yaml.safe_load(DUPLICATE_CHANNEL_IN_FLOW_TELEMETRY_CONFIG)
148
+ )
149
+ yaml_config = _validate_yaml(raw_yaml_config)
150
+
151
+ mock_read_and_validate = mocker.patch(_mock_path(read_and_validate))
152
+ mock_read_and_validate.return_value = yaml_config
153
+
154
+ mock_load_named_expression_modules = mocker.patch(_mock_path(load_named_expression_modules))
155
+ mock_load_named_expression_modules.return_value = {
156
+ "log_substring_contains": "contains($1, $substr)",
157
+ "kinetic_energy_gt": "0.5 * $mass * $1 * $1 > $threshold",
158
+ }
159
+
160
+ dummy_yaml_path = Path()
161
+ dummy_named_expr_mod_path = Path()
162
+
163
+ with pytest.raises(TelemetryConfigValidationError, match="Can't have two identical channels"):
164
+ _ = TelemetryConfig.try_from_yaml(dummy_yaml_path, [dummy_named_expr_mod_path])
165
+
166
+
167
+ def test_telemetry_config_named_expression_interpolation():
168
+ pass
169
+
170
+
171
+ def test_telemetry_config_validations_duplicate_rules():
172
+ channel = ChannelConfig(
173
+ name="my_channel",
174
+ data_type=ChannelDataType.DOUBLE,
175
+ )
176
+
177
+ rule_on_my_channel_a = RuleConfig(
178
+ name="rule_a",
179
+ description="",
180
+ expression="$1 > 10",
181
+ channel_references=[
182
+ {"channel_reference": "$1", "channel_identifier": channel.fqn()},
183
+ ],
184
+ action=RuleActionCreateDataReviewAnnotation(
185
+ assignee="bob@example.com",
186
+ tags=["barometer"],
187
+ ),
188
+ )
189
+
190
+ another_rule_on_my_channel_a = RuleConfig(
191
+ name="rule_a", # same name
192
+ description="",
193
+ expression="$1 > 11",
194
+ channel_references=[
195
+ {"channel_reference": "$1", "channel_identifier": channel.fqn()},
196
+ ],
197
+ action=RuleActionCreateDataReviewAnnotation(
198
+ assignee="bob@example.com",
199
+ tags=["barometer"],
200
+ ),
201
+ )
202
+
203
+ with pytest.raises(TelemetryConfigValidationError, match="Can't have two rules"):
204
+ TelemetryConfig(
205
+ asset_name="my_asset",
206
+ ingestion_client_key="my_asset_key",
207
+ organization_id="my_organization_id",
208
+ flows=[
209
+ FlowConfig(
210
+ name="my_flow",
211
+ channels=[channel],
212
+ )
213
+ ],
214
+ rules=[rule_on_my_channel_a, another_rule_on_my_channel_a],
215
+ )
216
+
217
+
218
+ def test_telemetry_config_validations_duplicate_channels():
219
+ channel = ChannelConfig(
220
+ name="my_channel",
221
+ data_type=ChannelDataType.DOUBLE,
222
+ )
223
+
224
+ with pytest.raises(TelemetryConfigValidationError, match="Can't have two identical channels"):
225
+ TelemetryConfig(
226
+ asset_name="my_asset",
227
+ ingestion_client_key="my_asset_key",
228
+ organization_id="my_organization_id",
229
+ flows=[
230
+ FlowConfig(
231
+ name="my_flow",
232
+ channels=[
233
+ channel,
234
+ channel,
235
+ ],
236
+ )
237
+ ],
238
+ )
239
+
240
+
241
+ def test_telemetry_config_validations_flows_with_same_name():
242
+ channel = ChannelConfig(
243
+ name="my_channel",
244
+ data_type=ChannelDataType.DOUBLE,
245
+ )
246
+
247
+ channel_b = ChannelConfig(
248
+ name="my_other_channel",
249
+ data_type=ChannelDataType.DOUBLE,
250
+ )
251
+
252
+ with pytest.raises(TelemetryConfigValidationError, match="Can't have two flows"):
253
+ TelemetryConfig(
254
+ asset_name="my_asset",
255
+ ingestion_client_key="my_asset_key",
256
+ organization_id="my_organization_id",
257
+ flows=[
258
+ FlowConfig(
259
+ name="my_flow",
260
+ channels=[channel],
261
+ ),
262
+ FlowConfig(
263
+ name="my_flow",
264
+ channels=[channel_b],
265
+ ),
266
+ ],
267
+ )
268
+
269
+
270
+ TEST_YAML_CONFIG_STR = """
271
+ asset_name: LunarVehicle426
272
+ ingestion_client_key: lunar_vehicle_426
273
+
274
+ channels:
275
+ log_channel: &log_channel
276
+ name: log
277
+ data_type: string
278
+ description: asset logs
279
+
280
+ velocity_channel: &velocity_channel
281
+ name: velocity
282
+ data_type: double
283
+ description: speed
284
+ unit: Miles Per Hour
285
+ component: mainmotor
286
+
287
+ voltage_channel: &voltage_channel
288
+ name: voltage
289
+ data_type: int32
290
+ description: voltage at the source
291
+ unit: Volts
292
+
293
+ vehicle_state_channel: &vehicle_state_channel
294
+ name: vehicle_state
295
+ data_type: enum
296
+ description: vehicle state
297
+ unit: vehicle state
298
+ enum_types:
299
+ - name: Accelerating
300
+ key: 0
301
+ - name: Decelerating
302
+ key: 1
303
+ - name: Stopped
304
+ key: 2
305
+
306
+ gpio_channel: &gpio_channel
307
+ name: gpio
308
+ data_type: bit_field
309
+ description: on/off values for pins on gpio
310
+ bit_field_elements:
311
+ - name: 12v
312
+ index: 0
313
+ bit_count: 1
314
+ - name: charge
315
+ index: 1
316
+ bit_count: 2
317
+ - name: led
318
+ index: 3
319
+ bit_count: 4
320
+ - name: heater
321
+ index: 7
322
+ bit_count: 1
323
+
324
+ rules:
325
+ - name: overheating
326
+ description: Checks for vehicle overheating
327
+ expression: $1 == "Accelerating" && $2 > 80
328
+ channel_references:
329
+ - $1: *vehicle_state_channel
330
+ - $2: *voltage_channel
331
+ type: review
332
+
333
+ - name: speeding
334
+ description: Checks high vehicle speed
335
+ type: phase
336
+ expression: $1 > 20
337
+ channel_references:
338
+ - $1: *velocity_channel
339
+
340
+ - name: failures
341
+ description: Checks for failure logs
342
+ type: review
343
+ assignee: homer@example.com
344
+ expression:
345
+ name: log_substring_contains
346
+ channel_references:
347
+ - $1: *log_channel
348
+ sub_expressions:
349
+ - $substr: ERROR
350
+ tags:
351
+ - foo
352
+ - bar
353
+ - baz
354
+
355
+ - name: kinetic_energy
356
+ description: Tracks high energy output while in motion
357
+ type: review
358
+ assignee: homer@example.com
359
+ expression:
360
+ name: kinetic_energy_gt
361
+ channel_references:
362
+ - $1: *velocity_channel
363
+ sub_expressions:
364
+ - $mass: 10
365
+ - $threshold: 470
366
+ tags:
367
+ - nostromo
368
+
369
+ flows:
370
+ - name: readings
371
+ channels:
372
+ - <<: *velocity_channel
373
+ - <<: *voltage_channel
374
+ - <<: *vehicle_state_channel
375
+ - <<: *gpio_channel
376
+
377
+ - name: partial_readings
378
+ channels:
379
+ - <<: *velocity_channel
380
+ - <<: *voltage_channel
381
+
382
+ - name: logs
383
+ channels:
384
+ - <<: *log_channel
385
+
386
+ """
387
+
388
+ DUPLICATE_CHANNEL_IN_FLOW_TELEMETRY_CONFIG = """
389
+ asset_name: LunarVehicle426
390
+ ingestion_client_key: lunar_vehicle_426
391
+
392
+ channels:
393
+ velocity_channel: &velocity_channel
394
+ name: velocity
395
+ data_type: double
396
+ description: speed
397
+ unit: Miles Per Hour
398
+ component: mainmotor
399
+
400
+ flows:
401
+ - name: readings
402
+ channels:
403
+ - <<: *velocity_channel
404
+ - <<: *velocity_channel
405
+ """
File without changes