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,34 @@
1
+ import pytest
2
+ from pydantic_core import ValidationError
3
+
4
+ from sift_py.report_templates.config import ReportTemplateConfig
5
+
6
+
7
+ @pytest.fixture
8
+ def report_template_dict() -> dict:
9
+ return {
10
+ "name": "report-template",
11
+ "template_client_key": "template-client-key",
12
+ }
13
+
14
+
15
+ def test_report_template_config(report_template_dict):
16
+ report_template_config = ReportTemplateConfig(**report_template_dict)
17
+ assert report_template_config.name == "report-template"
18
+ assert report_template_config.template_client_key == "template-client-key"
19
+ assert report_template_config.organization_id == ""
20
+ assert report_template_config.tags is None
21
+ assert report_template_config.description is None
22
+ assert report_template_config.rule_client_keys == []
23
+ assert report_template_config.archived_date is None
24
+
25
+
26
+ def test_report_template_config_validation(report_template_dict):
27
+ report_template_dict.pop("name")
28
+ with pytest.raises(ValidationError, match="Field required"):
29
+ ReportTemplateConfig(**report_template_dict)
30
+
31
+ report_template_dict["name"] = "report-template"
32
+ report_template_dict.pop("template_client_key")
33
+ with pytest.raises(ValidationError, match="Field required"):
34
+ ReportTemplateConfig(**report_template_dict)
@@ -0,0 +1,94 @@
1
+ from unittest import mock
2
+
3
+ import pytest
4
+ from sift.report_templates.v1.report_templates_pb2 import ReportTemplate
5
+
6
+ from sift_py._internal.test_util.channel import MockChannel
7
+ from sift_py.report_templates.config import ReportTemplateConfig
8
+ from sift_py.report_templates.service import ReportTemplateService
9
+
10
+
11
+ @pytest.fixture
12
+ def report_template_service():
13
+ return ReportTemplateService(MockChannel())
14
+
15
+
16
+ def test_report_template_service_get_report_template_by_client_key(report_template_service):
17
+ report_template_client_key = "report-template-client-key"
18
+
19
+ with mock.patch.object(
20
+ ReportTemplateService,
21
+ "_get_report_template_by_client_key",
22
+ return_value=ReportTemplate(name="abc"),
23
+ ) as mock_get_report_template_by_client_key:
24
+ report_template_service.get_report_template(client_key=report_template_client_key)
25
+ mock_get_report_template_by_client_key.assert_called_once_with(report_template_client_key)
26
+
27
+
28
+ def test_report_template_service_get_report_template_by_id(report_template_service):
29
+ report_template_id = "report-template-id"
30
+
31
+ with mock.patch.object(
32
+ ReportTemplateService, "_get_report_template_by_id", return_value=ReportTemplate(name="abc")
33
+ ) as mock_get_report_template_by_id:
34
+ report_template_service.get_report_template(id=report_template_id)
35
+ mock_get_report_template_by_id.assert_called_once_with(report_template_id)
36
+
37
+
38
+ def test_report_template_service_get_report_template_missing_or_both_client_key_and_id(
39
+ report_template_service,
40
+ ):
41
+ with pytest.raises(ValueError, match="One of client_key or id must be provided"):
42
+ report_template_service.get_report_template()
43
+ with pytest.raises(ValueError, match="One of client_key or id must be provided"):
44
+ report_template_service.get_report_template(client_key="abc", id="abc")
45
+
46
+
47
+ def test_report_template_service_create_report_template(report_template_service):
48
+ report_template_config = ReportTemplateConfig(
49
+ name="report-template",
50
+ template_client_key="template-client-key",
51
+ )
52
+
53
+ with mock.patch.object(
54
+ ReportTemplateService, "_create_report_template"
55
+ ) as mock_create_report_template:
56
+ report_template_service.create_or_update_report_template(report_template_config)
57
+ mock_create_report_template.assert_called_once_with(report_template_config)
58
+
59
+
60
+ def test_report_template_service_update_report_template(report_template_service):
61
+ report_template_config = ReportTemplateConfig(
62
+ name="report-template",
63
+ template_client_key="template-client-key",
64
+ )
65
+
66
+ report_template_config_update = ReportTemplateConfig(
67
+ name="report-template-updated",
68
+ template_client_key="template-client-key",
69
+ )
70
+
71
+ with mock.patch.object(
72
+ ReportTemplateService, "_update_report_template"
73
+ ) as mock_update_report_template:
74
+ with mock.patch.object(
75
+ ReportTemplateService, "_get_report_template_by_client_key"
76
+ ) as mock_get_report_template_by_client_key:
77
+ mock_get_report_template_by_client_key.return_value = report_template_config
78
+ report_template_service.create_or_update_report_template(report_template_config_update)
79
+ mock_update_report_template.assert_called_once_with(
80
+ report_template_config_update, report_template_config
81
+ )
82
+
83
+
84
+ def test_report_template_service_missing_template_client_key(report_template_service):
85
+ report_template_config = ReportTemplateConfig.model_construct( # Without model validation
86
+ name="report-template",
87
+ template_client_key="",
88
+ )
89
+
90
+ with pytest.raises(Exception):
91
+ report_template_service.create_or_update_report_template(
92
+ report_template_config,
93
+ match="Report template report-template requires a template_client_key",
94
+ )
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any, List, Optional
5
+
6
+ from pydantic import BaseModel, ConfigDict
7
+
8
+ from sift_py._internal.convert.json import AsJson
9
+
10
+
11
+ class ReportTemplateConfig(BaseModel, AsJson):
12
+ """
13
+ Configuration for a report template.
14
+
15
+ - `name`: Name of the report template.
16
+ - `template_client_key`: Unique client key to identify the report template.
17
+ - `organization_id`: Organization ID that the report template belongs to.
18
+ - `tags`: Tags to associate with the report template.
19
+ - `description`: Description of the report template.
20
+ - `rule_client_keys`: List of rule client keys associated with the report template.
21
+ - `archived_date`: Date when the report template was archived. Setting this field
22
+ will archive the report template, and unsetting it will unarchive the report template.
23
+ """
24
+
25
+ model_config = ConfigDict(arbitrary_types_allowed=True)
26
+
27
+ name: str
28
+ template_client_key: str
29
+ organization_id: str = ""
30
+ tags: Optional[List[str]] = None
31
+ description: Optional[str] = None
32
+ rule_client_keys: List[str] = []
33
+ archived_date: Optional[datetime] = None
34
+
35
+ def as_json(self) -> Any:
36
+ return self.model_dump_json()
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import List, Optional, cast
5
+
6
+ from google.protobuf.field_mask_pb2 import FieldMask
7
+ from sift.report_templates.v1.report_templates_pb2 import (
8
+ CreateReportTemplateRequest,
9
+ CreateReportTemplateRequestClientKeys,
10
+ GetReportTemplateRequest,
11
+ GetReportTemplateResponse,
12
+ ReportTemplate,
13
+ ReportTemplateRule,
14
+ ReportTemplateTag,
15
+ UpdateReportTemplateRequest,
16
+ )
17
+ from sift.report_templates.v1.report_templates_pb2_grpc import ReportTemplateServiceStub
18
+
19
+ from sift_py._internal.time import to_timestamp_pb
20
+ from sift_py.grpc.transport import SiftChannel
21
+ from sift_py.report_templates.config import ReportTemplateConfig
22
+ from sift_py.yaml.report_templates import load_report_templates
23
+
24
+
25
+ class ReportTemplateService:
26
+ """
27
+ A service for managing report templates. Allows for creating, updating, and retrieving report
28
+ templates.
29
+ """
30
+
31
+ _report_template_service_stub: ReportTemplateServiceStub
32
+
33
+ def __init__(self, channel: SiftChannel):
34
+ self._report_template_service_stub = ReportTemplateServiceStub(channel)
35
+
36
+ def create_or_update_report_template(self, config: ReportTemplateConfig):
37
+ """
38
+ Create or update a report template via a ReportTemplateConfig. The config must contain a
39
+ template_client_key, otherwise an exception will be raised. If a report template with the
40
+ same client key exists, it will be updated. Otherwise, a new report template will be created.
41
+ See `sift_py.report_templates.config.ReportTemplateConfig` for more information on available
42
+ fields to configure.
43
+ """
44
+ if not config.template_client_key:
45
+ raise Exception(f"Report template {config.name} requires a template_client_key")
46
+
47
+ report_template = self._get_report_template_by_client_key(config.template_client_key)
48
+ if report_template:
49
+ self._update_report_template(config, report_template)
50
+ else:
51
+ self._create_report_template(config)
52
+
53
+ def get_report_template(
54
+ self, client_key: Optional[str] = None, id: Optional[str] = None
55
+ ) -> Optional[ReportTemplateConfig]:
56
+ """
57
+ Retrieve a report template by client key or id. Only one of client_key or id should be
58
+ provided, otherwise an exception will be raised. If a report template is found, it will be
59
+ returned as a ReportTemplateConfig object.
60
+ """
61
+ report_template = None
62
+ if (not client_key and not id) or (client_key and id):
63
+ raise ValueError("One of client_key or id must be provided")
64
+
65
+ if id:
66
+ report_template = self._get_report_template_by_id(id)
67
+ elif client_key:
68
+ report_template = self._get_report_template_by_client_key(client_key)
69
+
70
+ return (
71
+ ReportTemplateConfig(
72
+ name=report_template.name,
73
+ template_client_key=report_template.client_key,
74
+ organization_id=report_template.organization_id,
75
+ tags=[tag.tag_name for tag in report_template.tags],
76
+ description=report_template.description,
77
+ rule_client_keys=[rule.client_key for rule in report_template.rules],
78
+ archived_date=report_template.archived_date.ToDatetime()
79
+ if report_template.archived_date
80
+ else None,
81
+ )
82
+ if report_template
83
+ else None
84
+ )
85
+
86
+ def load_report_templates_from_yaml(self, paths: List[Path]) -> List[ReportTemplateConfig]:
87
+ """
88
+ Load report templates from YAML definitions. The YAML defined report template must have
89
+ a client key. If the report template with the given client key exists, it will be updated,
90
+ otherwise a new report template will be created.
91
+ See `sift_py.yaml.report_templates.load_report_templates` for more information on the YAML
92
+ spec for report templates.
93
+ """
94
+ report_templates = load_report_templates(paths)
95
+ [
96
+ self.create_or_update_report_template(report_template)
97
+ for report_template in report_templates
98
+ ]
99
+ return report_templates
100
+
101
+ def _get_report_template_by_id(self, report_template_id: str) -> Optional[ReportTemplate]:
102
+ req = GetReportTemplateRequest(report_template_id=report_template_id)
103
+ try:
104
+ res = cast(
105
+ GetReportTemplateResponse, self._report_template_service_stub.GetReportTemplate(req)
106
+ )
107
+ return cast(ReportTemplate, res.report_template) or None
108
+ except:
109
+ return None
110
+
111
+ def _get_report_template_by_client_key(self, client_key: str) -> Optional[ReportTemplate]:
112
+ req = GetReportTemplateRequest(client_key=client_key)
113
+ try:
114
+ res = cast(
115
+ GetReportTemplateResponse, self._report_template_service_stub.GetReportTemplate(req)
116
+ )
117
+ return res.report_template or None
118
+ except:
119
+ return None
120
+
121
+ def _create_report_template(self, config: ReportTemplateConfig):
122
+ client_keys_req = CreateReportTemplateRequestClientKeys(
123
+ rule_client_keys=config.rule_client_keys
124
+ )
125
+ req = CreateReportTemplateRequest(
126
+ name=config.name,
127
+ client_key=config.template_client_key,
128
+ description=config.description,
129
+ tag_names=config.tags,
130
+ organization_id=config.organization_id,
131
+ rule_client_keys=client_keys_req,
132
+ )
133
+ self._report_template_service_stub.CreateReportTemplate(req)
134
+
135
+ def _update_report_template(
136
+ self, config: ReportTemplateConfig, report_template: ReportTemplate
137
+ ):
138
+ """
139
+ Uses the report template id, organization id, and client key from the existing report
140
+ template. Updates the name, description, tags, rules, and archived date from the config,
141
+ if they are provided. Always passes all fields to the field mask for updating.
142
+ """
143
+ tags = []
144
+ if config.tags:
145
+ tags = [ReportTemplateTag(tag_name=tag) for tag in config.tags]
146
+
147
+ archived_date = None
148
+ if config.archived_date:
149
+ archived_date = to_timestamp_pb(config.archived_date)
150
+
151
+ rules = [
152
+ ReportTemplateRule(client_key=client_key) for client_key in config.rule_client_keys
153
+ ]
154
+
155
+ updated_report_template = ReportTemplate(
156
+ report_template_id=report_template.report_template_id,
157
+ organization_id=report_template.organization_id,
158
+ client_key=report_template.client_key,
159
+ name=config.name,
160
+ description=config.description,
161
+ tags=tags,
162
+ rules=rules,
163
+ archived_date=archived_date,
164
+ )
165
+
166
+ field_mask = FieldMask(paths=["name", "description", "tags", "rules", "archived_date"])
167
+ self._report_template_service_stub.UpdateReportTemplate(
168
+ UpdateReportTemplateRequest(
169
+ report_template=updated_report_template, update_mask=field_mask
170
+ )
171
+ )
sift_py/rest.py ADDED
@@ -0,0 +1,29 @@
1
+ from typing import TypedDict
2
+
3
+ from typing_extensions import NotRequired
4
+
5
+ from sift_py.grpc.transport import _clean_uri
6
+
7
+
8
+ class SiftRestConfig(TypedDict):
9
+ """
10
+ Config class used to to interact with services that use Sift's REST API.`.
11
+ - `uri`: The URI of Sift's REST API. The scheme portion of the URI i.e. `https://` should be ommitted.
12
+ - `apikey`: User-generated API key generated via the Sift application.
13
+ - `use_ssl`: INTERNAL USE. Meant to be used for local development.
14
+ """
15
+
16
+ uri: str
17
+ apikey: str
18
+ use_ssl: NotRequired[bool]
19
+
20
+
21
+ def compute_uri(restconf: SiftRestConfig) -> str:
22
+ uri = restconf["uri"]
23
+ use_ssl = restconf.get("use_ssl", True)
24
+ clean_uri = _clean_uri(uri, use_ssl)
25
+
26
+ if use_ssl:
27
+ return f"https://{clean_uri}"
28
+
29
+ return f"http://{clean_uri}"
File without changes
@@ -0,0 +1,109 @@
1
+ from sift_py.ingestion.channel import ChannelConfig, ChannelDataType
2
+
3
+ from .config import (
4
+ RuleActionCreateDataReviewAnnotation,
5
+ RuleActionCreatePhaseAnnotation,
6
+ RuleConfig,
7
+ )
8
+
9
+
10
+ def test_rule_config_json():
11
+ voltage_rule_expression = "$1 > 10"
12
+ voltage_rule_config = RuleConfig(
13
+ name="High Voltage",
14
+ description="Rock & Roll",
15
+ expression=voltage_rule_expression,
16
+ action=RuleActionCreatePhaseAnnotation(),
17
+ channel_references=[
18
+ {
19
+ "channel_reference": "$1",
20
+ "channel_config": ChannelConfig(
21
+ name="voltage",
22
+ data_type=ChannelDataType.DOUBLE,
23
+ ),
24
+ }
25
+ ],
26
+ )
27
+ assert voltage_rule_config.expression == voltage_rule_expression
28
+
29
+ overheating_rule_expression = '$1 == "Accelerating" && $2 > $3'
30
+ overheating_rule_config = RuleConfig(
31
+ name="overheating",
32
+ description="checks if vehicle overheats while accelerating",
33
+ expression=overheating_rule_expression,
34
+ action=RuleActionCreateDataReviewAnnotation(
35
+ tags=["foo", "bar"],
36
+ assignee="foobar@baz.com",
37
+ ),
38
+ channel_references=[
39
+ {
40
+ "channel_reference": "$1",
41
+ "channel_config": ChannelConfig(
42
+ name="vehicle_state",
43
+ component="motor",
44
+ data_type=ChannelDataType.INT_32,
45
+ ),
46
+ },
47
+ {
48
+ "channel_reference": "$2",
49
+ "channel_config": ChannelConfig(
50
+ name="temperature",
51
+ component="motor",
52
+ data_type=ChannelDataType.INT_32,
53
+ ),
54
+ },
55
+ ],
56
+ sub_expressions={
57
+ "$3": 80,
58
+ },
59
+ )
60
+ assert overheating_rule_config.expression == '$1 == "Accelerating" && $2 > 80'
61
+
62
+ contains_rule_expression = "contains($1, $2)"
63
+ contains_rule_config = RuleConfig(
64
+ name="contains",
65
+ description="checks if vehicle overheats while accelerating",
66
+ expression=contains_rule_expression,
67
+ action=RuleActionCreateDataReviewAnnotation(
68
+ tags=["foo", "bar"],
69
+ assignee="foobar@baz.com",
70
+ ),
71
+ channel_references=[
72
+ {
73
+ "channel_reference": "$1",
74
+ "channel_config": ChannelConfig(
75
+ name="log",
76
+ data_type=ChannelDataType.INT_32,
77
+ ),
78
+ },
79
+ ],
80
+ sub_expressions={
81
+ "$2": "Error",
82
+ },
83
+ )
84
+ assert contains_rule_config.expression == 'contains($1, "Error")'
85
+
86
+
87
+ def test_rule_named_expressions():
88
+ kinetic_energy_gt_expression = "0.5 * $mass * $1 * $1 > $threshold"
89
+
90
+ rule_on_kinetic_energy = RuleConfig(
91
+ name="rule_onkinetic_energy",
92
+ description="checks high periods of energy output",
93
+ expression=kinetic_energy_gt_expression,
94
+ action=RuleActionCreatePhaseAnnotation(),
95
+ channel_references=[
96
+ {
97
+ "channel_reference": "$1",
98
+ "channel_config": ChannelConfig(
99
+ name="velocity",
100
+ data_type=ChannelDataType.INT_32,
101
+ ),
102
+ },
103
+ ],
104
+ sub_expressions={
105
+ "$mass": 10,
106
+ "$threshold": 35,
107
+ },
108
+ )
109
+ assert rule_on_kinetic_energy.expression == "0.5 * 10 * $1 * $1 > 35"
@@ -0,0 +1,168 @@
1
+ from unittest import mock
2
+
3
+ import pytest
4
+ from sift.assets.v1.assets_pb2 import Asset
5
+ from sift.rules.v1.rules_pb2 import Rule
6
+
7
+ from sift_py._internal.test_util.channel import MockChannel
8
+ from sift_py.rule.config import RuleActionCreateDataReviewAnnotation, RuleConfig
9
+ from sift_py.rule.service import RuleService
10
+
11
+
12
+ @pytest.fixture()
13
+ def rule_service():
14
+ return RuleService(MockChannel())
15
+
16
+
17
+ def test_rule_service_get_rule_from_client_key(rule_service):
18
+ rule_client_key = "rule-client-key"
19
+
20
+ with mock.patch.object(
21
+ RuleService,
22
+ "_get_rule_from_client_key",
23
+ return_value=Rule(name="abc"),
24
+ ) as mock_get_rule_from_client_key:
25
+ rule_service.get_rule(rule_client_key)
26
+ mock_get_rule_from_client_key.assert_called_once_with(rule_client_key)
27
+
28
+
29
+ def test_rule_service_get_rule_from_rule_id(rule_service):
30
+ rule_id = "rule-id"
31
+
32
+ with mock.patch.object(
33
+ RuleService, "_get_rule_from_rule_id", return_value=Rule(name="abc")
34
+ ) as mock_get_rule_from_rule_id:
35
+ rule_service.get_rule(rule_id)
36
+ mock_get_rule_from_rule_id.assert_called_once_with(rule_id)
37
+
38
+
39
+ def test_rule_service_get_rule_missing_client_key_and_id(rule_service):
40
+ with mock.patch.object(
41
+ RuleService,
42
+ "_get_rule_from_client_key",
43
+ return_value=None,
44
+ ) as mock_get_rule_from_client_key:
45
+ with mock.patch.object(
46
+ RuleService, "_get_rule_from_rule_id", return_value=None
47
+ ) as mock_get_rule_from_rule_id:
48
+ rule = rule_service.get_rule("")
49
+ mock_get_rule_from_client_key.asset_called_once()
50
+ mock_get_rule_from_rule_id.asset_called_once()
51
+ assert rule is None
52
+
53
+
54
+ def test_rule_service_create_rule(rule_service):
55
+ rule = RuleConfig(
56
+ name="rule",
57
+ rule_client_key="rule-client-key",
58
+ channel_references=[],
59
+ )
60
+
61
+ with mock.patch.object(RuleService, "_create_rule") as mock_create_rule:
62
+ rule_service.create_or_update_rule(rule)
63
+ mock_create_rule.assert_called_once_with(rule)
64
+
65
+
66
+ def test_rule_service_update_rule(rule_service):
67
+ rule = RuleConfig(
68
+ name="rule",
69
+ rule_client_key="rule-client-key",
70
+ channel_references=[],
71
+ )
72
+
73
+ with mock.patch.object(RuleService, "_update_rule") as mock_update_rule:
74
+ with mock.patch.object(
75
+ RuleService, "_get_rule_from_client_key", return_value=Rule(name=rule.name)
76
+ ) as mock_get_rule_from_client_key:
77
+ rule_service.create_or_update_rule(rule)
78
+ mock_get_rule_from_client_key.assert_called_once_with(rule.rule_client_key)
79
+ mock_update_rule.assert_called_once()
80
+
81
+
82
+ def test_rule_service_load_rules_from_yaml(rule_service):
83
+ rule_yaml = {
84
+ "name": "rule",
85
+ "rule_client_key": "rule-client-key",
86
+ "channel_references": [{"$1": "channel"}],
87
+ "description": "description",
88
+ "expression": "$1 > 0",
89
+ "assignee": "assignee@abc.com",
90
+ "type": "review",
91
+ "asset_names": ["asset"],
92
+ }
93
+ with mock.patch.object(RuleService, "create_or_update_rule"):
94
+ with mock.patch(
95
+ "sift_py.rule.service.load_rule_modules",
96
+ return_value=[rule_yaml],
97
+ ):
98
+ rule_configs = rule_service.load_rules_from_yaml(["path/to/rules.yml"])
99
+ assert len(rule_configs) == 1
100
+
101
+ rule_config = rule_configs[0]
102
+ assert rule_config.name == rule_yaml["name"]
103
+ assert rule_config.rule_client_key == rule_yaml["rule_client_key"]
104
+ assert rule_config.channel_references == [
105
+ {
106
+ "channel_reference": "$1",
107
+ "channel_identifier": "channel",
108
+ }
109
+ ]
110
+ assert rule_config.description == rule_yaml["description"]
111
+ assert rule_config.expression == rule_yaml["expression"]
112
+ assert rule_config.action.assignee == rule_yaml["assignee"]
113
+ assert rule_config.asset_names == rule_yaml["asset_names"]
114
+ assert isinstance(rule_config.action, RuleActionCreateDataReviewAnnotation)
115
+
116
+
117
+ def test_rule_service_attach_asset():
118
+ rule_config = RuleConfig(
119
+ name="rule",
120
+ rule_client_key="rule-client-key",
121
+ expression="1",
122
+ channel_references=[],
123
+ action=RuleActionCreateDataReviewAnnotation(),
124
+ asset_names=["abc"],
125
+ )
126
+ asset = Asset(name="asset", asset_id="asset-id", organization_id="org-id")
127
+ with mock.patch("sift_py.rule.service.RuleServiceStub", return_value=mock.MagicMock()):
128
+ rule_service = RuleService(MockChannel())
129
+ with mock.patch.object(RuleService, "_get_assets", return_value=[asset]):
130
+ returned_config = rule_service.attach_asset(rule_config, ["asset"])
131
+ assert "abc" in returned_config.asset_names
132
+ assert "asset" in returned_config.asset_names
133
+
134
+
135
+ def test_rule_service_detach_asset():
136
+ rule_config = RuleConfig(
137
+ name="rule",
138
+ rule_client_key="rule-client-key",
139
+ expression="1",
140
+ channel_references=[],
141
+ action=RuleActionCreateDataReviewAnnotation(),
142
+ asset_names=["abc", "def"],
143
+ )
144
+ asset_abc = Asset(name="abc", asset_id="abc-id", organization_id="org-id")
145
+ asset_def = Asset(name="def", asset_id="def-id", organization_id="org-id")
146
+ with mock.patch("sift_py.rule.service.RuleServiceStub", return_value=mock.MagicMock()):
147
+ rule_service = RuleService(MockChannel())
148
+ with mock.patch.object(RuleService, "_get_assets", return_value=[asset_abc, asset_def]):
149
+ returned_config = rule_service.detach_asset(rule_config, ["abc"])
150
+ assert "abc" not in returned_config.asset_names
151
+ assert "def" in returned_config.asset_names
152
+
153
+
154
+ def test_rule_service_detach_asset_empty_list():
155
+ rule_config = RuleConfig(
156
+ name="rule",
157
+ rule_client_key="rule-client-key",
158
+ expression="1",
159
+ channel_references=[],
160
+ action=RuleActionCreateDataReviewAnnotation(),
161
+ asset_names=["asset"],
162
+ )
163
+ asset = Asset(name="asset", asset_id="asset-id", organization_id="org-id")
164
+ with mock.patch("sift_py.rule.service.RuleServiceStub", return_value=mock.MagicMock()):
165
+ rule_service = RuleService(MockChannel())
166
+ with mock.patch.object(RuleService, "_get_assets", return_value=[asset]):
167
+ with pytest.raises(ValueError, match="must be associated with at least one asset"):
168
+ rule_service.detach_asset(rule_config, ["asset"])