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,207 @@
1
+ import pytest
2
+
3
+ from sift_py.ingestion.config.yaml.error import YamlConfigError
4
+ from sift_py.yaml import rule
5
+
6
+
7
+ def test__validate_sub_expression():
8
+ rule._validate_sub_expression({"$mass": 10})
9
+ rule._validate_sub_expression({"$m": 10})
10
+
11
+ with pytest.raises(YamlConfigError, match="Invalid sub-expression key"):
12
+ rule._validate_sub_expression({"mass": 10})
13
+
14
+ with pytest.raises(YamlConfigError, match="Invalid sub-expression key"):
15
+ rule._validate_sub_expression({"$!mass": 10})
16
+
17
+ with pytest.raises(YamlConfigError, match="Invalid sub-expression key"):
18
+ rule._validate_sub_expression({"$$mass": 10})
19
+
20
+
21
+ def test__validate_rule():
22
+ rule._validate_rule(
23
+ {
24
+ "name": "overheat_rule",
25
+ "description": "some_description",
26
+ "expression": "$1 > 10 && $2 > 10",
27
+ "type": "review",
28
+ "assignee": "homer@example.com",
29
+ "tags": ["foo", "bar"],
30
+ "channel_references": [
31
+ {"$1": {"name": "voltage", "data_type": "double"}},
32
+ {"$2": {"name": "vehicle_state", "data_type": "double"}},
33
+ ],
34
+ "rule_client_key": "overheat_rule_key",
35
+ }
36
+ )
37
+
38
+ # Rule with tag and asset names
39
+ rule._validate_rule(
40
+ {
41
+ "name": "overheat_rule",
42
+ "description": "some_description",
43
+ "expression": "$1 > 10 && $2 > 10",
44
+ "type": "review",
45
+ "assignee": "homer@example.com",
46
+ "tags": ["foo", "bar"],
47
+ "channel_references": [
48
+ {"$1": {"name": "voltage", "data_type": "double"}},
49
+ {"$2": {"name": "vehicle_state", "data_type": "double"}},
50
+ ],
51
+ "asset_names": ["NostromoLV426"],
52
+ "tag_names": ["vehicle"],
53
+ "rule_client_key": "overheat_rule_key",
54
+ }
55
+ )
56
+
57
+ with pytest.raises(YamlConfigError, match="Expected 'name' to be <str> but it is <int>"):
58
+ rule._validate_rule(
59
+ {
60
+ "name": 0,
61
+ "description": "some_description",
62
+ "expression": "$1 > 10 && $2 > 10",
63
+ "type": "review",
64
+ "assignee": "homer@example.com",
65
+ "tags": ["foo", "bar"],
66
+ "channel_references": [
67
+ {"$1": {"name": "voltage", "data_type": "double"}},
68
+ {"$2": {"name": "vehicle_state", "data_type": "double"}},
69
+ ],
70
+ }
71
+ )
72
+
73
+ with pytest.raises(YamlConfigError, match="Expected 'description' to be <str> but it is <int>"):
74
+ rule._validate_rule(
75
+ {
76
+ "name": "overheat_rule",
77
+ "description": 0,
78
+ "expression": "$1 > 10 && $2 > 10",
79
+ "type": "review",
80
+ "assignee": "homer@example.com",
81
+ "tags": ["foo", "bar"],
82
+ "channel_references": [
83
+ {"$1": {"name": "voltage", "data_type": "double"}},
84
+ {"$2": {"name": "vehicle_state", "data_type": "double"}},
85
+ ],
86
+ }
87
+ )
88
+
89
+ with pytest.raises(
90
+ YamlConfigError, match="Expected 'expression' to be <<class 'str'> | <class 'dict'>>"
91
+ ):
92
+ rule._validate_rule(
93
+ {
94
+ "name": "overheat_rule",
95
+ "description": "some_description",
96
+ "expression": 123,
97
+ "type": "review",
98
+ "assignee": "homer@example.com",
99
+ "tags": ["foo", "bar"],
100
+ "channel_references": [
101
+ {"$1": {"name": "voltage", "data_type": "double"}},
102
+ {"$2": {"name": "vehicle_state", "data_type": "double"}},
103
+ ],
104
+ }
105
+ )
106
+
107
+ with pytest.raises(
108
+ YamlConfigError, match="Expected 'type' to be <review | phase> but it is <str>"
109
+ ):
110
+ rule._validate_rule(
111
+ {
112
+ "name": "overheat_rule",
113
+ "description": "foobar",
114
+ "expression": "$1 > 10 && $2 > 10",
115
+ "type": "not_valid_type",
116
+ "assignee": "homer@example.com",
117
+ "tags": ["foo", "bar"],
118
+ "channel_references": [
119
+ {"$1": {"name": "voltage", "data_type": "double"}},
120
+ {"$2": {"name": "vehicle_state", "data_type": "double"}},
121
+ ],
122
+ }
123
+ )
124
+
125
+ with pytest.raises(YamlConfigError, match="Expected 'tags' to be"):
126
+ rule._validate_rule(
127
+ {
128
+ "name": "overheat_rule",
129
+ "description": "foobar",
130
+ "expression": "$1 > 10 && $2 > 10",
131
+ "type": "phase",
132
+ "assignee": "homer@example.com",
133
+ "tags": 123,
134
+ "channel_references": [
135
+ {"$1": {"name": "voltage", "data_type": "double"}},
136
+ {"$2": {"name": "vehicle_state", "data_type": "double"}},
137
+ ],
138
+ }
139
+ )
140
+
141
+ with pytest.raises(YamlConfigError, match="Invalid channel reference key"):
142
+ rule._validate_rule(
143
+ {
144
+ "name": "overheat_rule",
145
+ "description": "foobar",
146
+ "expression": "$1 > 10 && $2 > 10",
147
+ "type": "phase",
148
+ "assignee": "homer@example.com",
149
+ "channel_references": [
150
+ {"$foo": {"name": "voltage", "data_type": "double"}},
151
+ {"$2": {"name": "vehicle_state", "data_type": "double"}},
152
+ ],
153
+ }
154
+ )
155
+
156
+ with pytest.raises(YamlConfigError, match="Invalid sub-expression key"):
157
+ rule._validate_rule(
158
+ {
159
+ "name": "overheat_rule",
160
+ "description": "foobar",
161
+ "expression": {"name": "kinetic_energy_gt"},
162
+ "type": "phase",
163
+ "assignee": "homer@example.com",
164
+ "channel_references": [
165
+ {"$1": {"name": "velocity", "data_type": "double"}},
166
+ ],
167
+ "sub_expressions": [
168
+ {"mass": None},
169
+ ],
170
+ }
171
+ )
172
+
173
+ with pytest.raises(YamlConfigError, match="Expected 'asset_names' to be"):
174
+ rule._validate_rule(
175
+ {
176
+ "name": "overheat_rule",
177
+ "description": "some_description",
178
+ "expression": "$1 > 10 && $2 > 10",
179
+ "type": "review",
180
+ "assignee": "homer@example.com",
181
+ "tags": ["foo", "bar"],
182
+ "channel_references": [
183
+ {"$1": {"name": "voltage", "data_type": "double"}},
184
+ {"$2": {"name": "vehicle_state", "data_type": "double"}},
185
+ ],
186
+ "rule_client_key": "overheat_rule_key",
187
+ "asset_names": 123,
188
+ }
189
+ )
190
+
191
+ with pytest.raises(YamlConfigError, match="Expected 'tag_names' to be"):
192
+ rule._validate_rule(
193
+ {
194
+ "name": "overheat_rule",
195
+ "description": "some_description",
196
+ "expression": "$1 > 10 && $2 > 10",
197
+ "type": "review",
198
+ "assignee": "homer@example.com",
199
+ "tags": ["foo", "bar"],
200
+ "channel_references": [
201
+ {"$1": {"name": "voltage", "data_type": "double"}},
202
+ {"$2": {"name": "vehicle_state", "data_type": "double"}},
203
+ ],
204
+ "rule_client_key": "overheat_rule_key",
205
+ "tag_names": 123,
206
+ }
207
+ )
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Dict, List, Literal, Union, cast
5
+
6
+ from typing_extensions import NotRequired, TypedDict
7
+
8
+ from sift_py.ingestion.channel import ChannelDataTypeStrRep
9
+ from sift_py.ingestion.config.yaml.error import YamlConfigError
10
+ from sift_py.yaml.utils import _type_fqn
11
+
12
+ _CHANNEL_REFERENCE_REGEX = re.compile(r"^\$\d+$")
13
+
14
+
15
+ def _validate_channel_anchor(val: Any):
16
+ if not isinstance(val, str):
17
+ raise YamlConfigError._invalid_property(
18
+ val,
19
+ "<str>",
20
+ "&str",
21
+ ["channels"],
22
+ )
23
+
24
+
25
+ def _validate_channel(val: Any):
26
+ channel = cast(Dict[Any, Any], val)
27
+
28
+ name = channel.get("name")
29
+
30
+ if not isinstance(name, str):
31
+ raise YamlConfigError._invalid_property(name, "- name", "str", ["channels"])
32
+
33
+ description = channel.get("description")
34
+
35
+ if description is not None and not isinstance(description, str):
36
+ raise YamlConfigError._invalid_property(description, "- description", "str", ["channels"])
37
+
38
+ unit = channel.get("unit")
39
+
40
+ if unit is not None and not isinstance(unit, str):
41
+ raise YamlConfigError._invalid_property(unit, "- unit", "str", ["channels"])
42
+
43
+ component = channel.get("component")
44
+
45
+ if component is not None and not isinstance(component, str):
46
+ raise YamlConfigError._invalid_property(component, "- component", "str", ["channels"])
47
+
48
+ data_type = channel.get("data_type")
49
+ valid_data_type_values = [v.value for v in ChannelDataTypeStrRep]
50
+
51
+ if not data_type in valid_data_type_values:
52
+ raise YamlConfigError._invalid_property(
53
+ data_type,
54
+ "- data_type",
55
+ " | ".join(valid_data_type_values),
56
+ ["channels"],
57
+ )
58
+
59
+ if data_type == ChannelDataTypeStrRep.ENUM.value:
60
+ enum_types = channel.get("enum_types")
61
+
62
+ if not isinstance(enum_types, list):
63
+ raise YamlConfigError._invalid_property(
64
+ enum_types,
65
+ "- enum_types",
66
+ f"List<{_type_fqn(ChannelEnumTypeYamlSpec)}>",
67
+ ["channels"],
68
+ )
69
+
70
+ for enum_type in cast(List[Any], enum_types):
71
+ _validate_enum_type(enum_type)
72
+
73
+ elif data_type == ChannelDataTypeStrRep.BIT_FIELD.value:
74
+ bit_field_elements = channel.get("bit_field_elements")
75
+
76
+ if not isinstance(bit_field_elements, list):
77
+ raise YamlConfigError._invalid_property(
78
+ bit_field_elements,
79
+ "- bit_field_elements",
80
+ f"List<{_type_fqn(ChannelBitFieldElementYamlSpec)}>",
81
+ )
82
+
83
+ for bit_field_element in cast(List[Any], bit_field_elements):
84
+ _validate_bit_field_element(bit_field_element)
85
+
86
+ else:
87
+ enum_types = channel.get("enum_types")
88
+
89
+ if enum_types is not None:
90
+ raise YamlConfigError(
91
+ f"Channel of data-type '{data_type}' should not have 'enum_types' set."
92
+ )
93
+
94
+ bit_field_elements = channel.get("bit_field_elements")
95
+
96
+ if bit_field_elements is not None:
97
+ raise YamlConfigError(
98
+ f"Channel of data-type '{data_type}' should not have 'bit_field_elements' set."
99
+ )
100
+
101
+
102
+ def _validate_enum_type(val: Any):
103
+ enum_type = cast(Dict[Any, Any], val)
104
+
105
+ name = enum_type.get("name")
106
+
107
+ if not isinstance(name, str):
108
+ raise YamlConfigError._invalid_property(
109
+ name,
110
+ "- name",
111
+ "str",
112
+ ["channels", "- enum_type"],
113
+ )
114
+
115
+ key = enum_type.get("key")
116
+
117
+ if not isinstance(key, int):
118
+ raise YamlConfigError._invalid_property(
119
+ key,
120
+ "- key",
121
+ "int",
122
+ ["channels", "- enum_type"],
123
+ )
124
+
125
+
126
+ def _validate_bit_field_element(val: Any):
127
+ bit_field_element = cast(Dict[Any, Any], val)
128
+
129
+ name = bit_field_element.get("name")
130
+
131
+ if not isinstance(name, str):
132
+ raise YamlConfigError._invalid_property(
133
+ name, "- name", "str", ["channels", "- bit_field_elements"]
134
+ )
135
+
136
+ index = bit_field_element.get("index")
137
+
138
+ if not isinstance(index, int):
139
+ raise YamlConfigError._invalid_property(
140
+ name, "- index", "int", ["channels", "- bit_field_elements"]
141
+ )
142
+
143
+ bit_count = bit_field_element.get("bit_count")
144
+
145
+ if not isinstance(bit_count, int):
146
+ raise YamlConfigError._invalid_property(
147
+ name, "- bit_count", "int", ["channels", "- bit_field_elements"]
148
+ )
149
+
150
+
151
+ def _validate_channel_reference(val: Any):
152
+ channel_reference = cast(Dict[Any, Any], val)
153
+
154
+ for key, value in channel_reference.items():
155
+ if not isinstance(key, str):
156
+ raise YamlConfigError._invalid_property(
157
+ channel_reference,
158
+ "- <str>",
159
+ f"Dict[str, {_type_fqn(ChannelConfigYamlSpec)}]",
160
+ ["rules", "- channel_references"],
161
+ )
162
+
163
+ if _CHANNEL_REFERENCE_REGEX.match(key) is None:
164
+ raise YamlConfigError(
165
+ f"Invalid channel reference key '{key}'. Expected an integer prefixed with '$' e.g. '$1', '$2', and so on."
166
+ )
167
+
168
+ if isinstance(value, dict): # Do this for YamlConfigChannelSpec but not str
169
+ try:
170
+ _validate_channel(value)
171
+ except YamlConfigError as err:
172
+ raise YamlConfigError(f"Rule '{key}' contains an invalid channel reference:\n{err}")
173
+
174
+
175
+ class ChannelConfigYamlSpec(TypedDict):
176
+ """
177
+ Formal spec that defines what a channel should look like in YAML.
178
+
179
+ `name`: Name of channel.
180
+ `description`: Optional channel description.
181
+ `unit`: Unit of measurement.
182
+ `component`: Name of component that channel belongs to.
183
+ `data_type`: Type of the data associated with the channel.
184
+ `enum_types`: Required if `data_type` is `enum.
185
+ `bit_field_elements`: Required if `data_type` is `bit_field`.
186
+ """
187
+
188
+ name: str
189
+ description: NotRequired[str]
190
+ unit: NotRequired[str]
191
+ component: NotRequired[str]
192
+ data_type: Union[
193
+ Literal["double"],
194
+ Literal["string"],
195
+ Literal["enum"],
196
+ Literal["bit_field"],
197
+ Literal["bool"],
198
+ Literal["float"],
199
+ Literal["int32"],
200
+ Literal["int64"],
201
+ Literal["uint32"],
202
+ Literal["uint64"],
203
+ ]
204
+ enum_types: NotRequired[List[ChannelEnumTypeYamlSpec]]
205
+ bit_field_elements: NotRequired[List[ChannelBitFieldElementYamlSpec]]
206
+
207
+
208
+ class ChannelEnumTypeYamlSpec(TypedDict):
209
+ """
210
+ Formal spec that defines what a channel enum type should look like in YAML.
211
+ """
212
+
213
+ name: str
214
+ key: int
215
+
216
+
217
+ class ChannelBitFieldElementYamlSpec(TypedDict):
218
+ """
219
+ Formal spec that defines what a bit-field element should look like in YAML.
220
+ """
221
+
222
+ name: str
223
+ index: int
224
+ bit_count: int
@@ -0,0 +1,73 @@
1
+ from datetime import datetime
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List, cast
4
+
5
+ import yaml
6
+ from typing_extensions import NotRequired, TypedDict
7
+
8
+ from sift_py.ingestion.config.yaml.error import YamlConfigError
9
+ from sift_py.report_templates.config import ReportTemplateConfig
10
+ from sift_py.yaml.utils import _handle_subdir
11
+
12
+
13
+ def load_report_templates(paths: List[Path]) -> List[ReportTemplateConfig]:
14
+ """
15
+ Takes in a list of paths to YAML files which contains report templates and processes them into a list of
16
+ `ReportTemplateConfig` objects. For more information on report templates see
17
+ `sift_py.report_templates.config.ReportTemplateConfig`.
18
+ """
19
+ report_templates: List[ReportTemplateConfig] = []
20
+
21
+ def update_report_templates(path: Path):
22
+ report_templates.extend(_read_report_template_yaml(path))
23
+
24
+ for path in paths:
25
+ if path.is_dir():
26
+ _handle_subdir(path, update_report_templates)
27
+ elif path.is_file():
28
+ update_report_templates(path)
29
+ return report_templates
30
+
31
+
32
+ def _read_report_template_yaml(path: Path) -> List[ReportTemplateConfig]:
33
+ report_templates = []
34
+ with open(path, "r") as f:
35
+ report_templates_yaml = cast(Dict[str, Any], yaml.safe_load(f.read()))
36
+
37
+ report_template_list = report_templates_yaml.get("report_templates")
38
+ if not isinstance(report_template_list, list):
39
+ raise YamlConfigError(
40
+ f"Expected 'report_templates' to be a list in report template yaml: '{path}'"
41
+ )
42
+
43
+ for report_template in report_template_list:
44
+ try:
45
+ report_template_config = ReportTemplateConfig(**report_template)
46
+ report_templates.append(report_template_config)
47
+ except Exception as e:
48
+ raise YamlConfigError(f"Error parsing report template '{report_template}'") from e
49
+
50
+ return report_templates
51
+
52
+
53
+ class ReportTemplateYamlSpec(TypedDict):
54
+ """
55
+ Formal spec for a report template.
56
+
57
+ `name`: Name of the report template.
58
+ `template_client_key`: Unique client key to identify the report template.
59
+ `organization_id`: Organization ID that the report template belongs to.
60
+ `tags`: Tags to associate with the report template.
61
+ `description`: Description of the report template.
62
+ `rule_client_keys`: List of rule client keys associated with the report template.
63
+ `archived_date`: Date when the report template was archived. Setting this field
64
+ will archive the report template, and unsetting it will unarchive the report template.
65
+ """
66
+
67
+ name: str
68
+ template_client_key: str
69
+ organization_id: NotRequired[str]
70
+ tags: NotRequired[List[str]]
71
+ description: NotRequired[str]
72
+ rule_client_keys: List[str]
73
+ archived_date: NotRequired[datetime]