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,484 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional, Union, cast
6
+
7
+ from sift.annotations.v1.annotations_pb2 import AnnotationType
8
+ from sift.assets.v1.assets_pb2 import Asset, ListAssetsRequest, ListAssetsResponse
9
+ from sift.assets.v1.assets_pb2_grpc import AssetServiceStub
10
+ from sift.channels.v2.channels_pb2_grpc import ChannelServiceStub
11
+ from sift.rules.v1.rules_pb2 import (
12
+ ANNOTATION,
13
+ AnnotationActionConfiguration,
14
+ CalculatedChannelConfig,
15
+ CreateRuleRequest,
16
+ GetRuleRequest,
17
+ GetRuleResponse,
18
+ Rule,
19
+ RuleActionConfiguration,
20
+ RuleAssetConfiguration,
21
+ RuleConditionExpression,
22
+ UpdateActionRequest,
23
+ UpdateConditionRequest,
24
+ UpdateRuleRequest,
25
+ )
26
+ from sift.rules.v1.rules_pb2_grpc import RuleServiceStub
27
+ from sift.users.v2.users_pb2_grpc import UserServiceStub
28
+
29
+ from sift_py._internal.cel import cel_in
30
+ from sift_py._internal.channel import get_channels
31
+ from sift_py._internal.user import get_active_users
32
+ from sift_py.grpc.transport import SiftChannel
33
+ from sift_py.ingestion._internal.channel import channel_reference_from_fqn
34
+ from sift_py.ingestion.channel import channel_fqn
35
+ from sift_py.rule.config import (
36
+ ExpressionChannelReference,
37
+ ExpressionChannelReferenceChannelConfig,
38
+ RuleAction,
39
+ RuleActionAnnotationKind,
40
+ RuleActionCreateDataReviewAnnotation,
41
+ RuleActionCreatePhaseAnnotation,
42
+ RuleActionKind,
43
+ RuleConfig,
44
+ )
45
+ from sift_py.yaml.rule import load_rule_modules
46
+
47
+
48
+ class RuleService:
49
+ """
50
+ A service for managing rules. Allows for loading rules from YAML and creating or updating them in the Sift API.
51
+ """
52
+
53
+ _asset_service_stub: AssetServiceStub
54
+ _channel_service_stub: ChannelServiceStub
55
+ _rule_service_stub: RuleServiceStub
56
+ _user_service_stub: UserServiceStub
57
+
58
+ def __init__(self, channel: SiftChannel):
59
+ self._asset_service_stub = AssetServiceStub(channel)
60
+ self._channel_service_stub = ChannelServiceStub(channel)
61
+ self._rule_service_stub = RuleServiceStub(channel)
62
+ self._user_service_stub = UserServiceStub(channel)
63
+
64
+ def load_rules_from_yaml(
65
+ self,
66
+ paths: List[Path],
67
+ named_expressions: Optional[Dict[str, str]] = None,
68
+ ) -> List[RuleConfig]:
69
+ """
70
+ Loads rules from a YAML spec, and creates or updates the rules in the Sift API.
71
+ For more on rule YAML definitions, see `sift_py.ingestion.config.yaml.spec.RuleYamlSpec`.
72
+ """
73
+ module_rules = load_rule_modules(paths)
74
+
75
+ rule_configs = []
76
+ for rule_yaml in module_rules:
77
+ rule_name = rule_yaml["name"]
78
+
79
+ # First parse channel references
80
+ yaml_channel_references = rule_yaml.get("channel_references", [])
81
+
82
+ rule_channel_references: List[
83
+ Union[ExpressionChannelReference, ExpressionChannelReferenceChannelConfig]
84
+ ] = []
85
+
86
+ for channel_ref in yaml_channel_references:
87
+ for ref, channel_config in channel_ref.items():
88
+ if isinstance(channel_config, dict):
89
+ name = channel_config.get("name", "")
90
+ component = channel_config.get("component", "")
91
+ elif isinstance(channel_config, str):
92
+ channel_reference = channel_reference_from_fqn(channel_config)
93
+ name = channel_reference.name
94
+ component = channel_reference.component
95
+ else:
96
+ raise ValueError(
97
+ f"Channel reference '{channel_config}' must be a string or a ChannelConfigYamlSpec"
98
+ )
99
+
100
+ rule_channel_references.append(
101
+ {
102
+ "channel_reference": ref,
103
+ "channel_identifier": channel_fqn(
104
+ {
105
+ "channel_name": name,
106
+ "component": component,
107
+ }
108
+ ),
109
+ }
110
+ )
111
+
112
+ if not rule_channel_references:
113
+ raise ValueError(f"Rule of name '{rule_yaml['name']}' requires channel_references")
114
+
115
+ # Parse expression for named expressions and sub expressions
116
+ expression = rule_yaml["expression"]
117
+ if isinstance(expression, dict):
118
+ expression_name = expression.get("name", "")
119
+ if not named_expressions:
120
+ raise ValueError(
121
+ f"Rule '{rule_name}' requires named expressions, but none were provided."
122
+ )
123
+ expression = named_expressions.get(expression_name, "")
124
+ if not expression:
125
+ raise ValueError(f"Named expression '{expression_name}' could not be found.")
126
+
127
+ yaml_subexprs = rule_yaml.get("sub_expressions", [])
128
+ subexpr: Dict[str, Any] = {}
129
+ for sub in yaml_subexprs:
130
+ for iden, value in sub.items():
131
+ subexpr[iden] = value
132
+
133
+ # Create rule actions
134
+ tags = rule_yaml.get("tags", [])
135
+ annotation_type = RuleActionAnnotationKind.from_str(rule_yaml["type"])
136
+ action: RuleAction = RuleActionCreatePhaseAnnotation(tags)
137
+ if annotation_type == RuleActionAnnotationKind.REVIEW:
138
+ action = RuleActionCreateDataReviewAnnotation(
139
+ assignee=rule_yaml.get("assignee"),
140
+ tags=tags,
141
+ )
142
+
143
+ # Append rule config to list
144
+ rule_configs.append(
145
+ RuleConfig(
146
+ name=rule_name,
147
+ rule_client_key=rule_yaml.get("rule_client_key"),
148
+ description=rule_yaml.get("description", ""),
149
+ expression=str(expression),
150
+ action=action,
151
+ channel_references=rule_channel_references,
152
+ asset_names=rule_yaml.get("asset_names", []),
153
+ sub_expressions=subexpr,
154
+ )
155
+ )
156
+
157
+ # Create all the rules
158
+ for rule_config in rule_configs:
159
+ self.create_or_update_rule(rule_config)
160
+
161
+ return rule_configs
162
+
163
+ def create_or_update_rules(self, rule_configs: List[RuleConfig]):
164
+ """
165
+ Create or update a list of rules via a list of RuleConfigs.
166
+ See `sift_py.rule.config.RuleConfig` for more information on configuation parameters for rules.
167
+ """
168
+ for config in rule_configs:
169
+ self.create_or_update_rule(config)
170
+
171
+ def attach_asset(self, rule: Union[str, RuleConfig], asset_names: List[str]) -> RuleConfig:
172
+ """
173
+ Associates a rule with an asset by name. The asset must already exist in the Sift API.
174
+ The provided rule may either be a rule client key, rule id, or a RuleConfig.
175
+ """
176
+ return self._attach_or_detach_asset(rule, asset_names, attach=True)
177
+
178
+ def detach_asset(self, rule: Union[str, RuleConfig], asset_names: List[str]) -> RuleConfig:
179
+ """
180
+ Disassociates a rule from an asset by name. The asset must already exist in the Sift API.
181
+ The provided rule may either be a rule client key, rule id, or a RuleConfig.
182
+ """
183
+ return self._attach_or_detach_asset(rule, asset_names, attach=False)
184
+
185
+ def _attach_or_detach_asset(
186
+ self, rule: Union[str, RuleConfig], asset_names: List[str], attach: bool
187
+ ) -> RuleConfig:
188
+ assets = self._get_assets(names=asset_names)
189
+ if not assets:
190
+ raise ValueError(
191
+ f"Cannot find all assets in list '{asset_names}'. One of these assets does not exist."
192
+ )
193
+
194
+ if isinstance(rule, str):
195
+ rule = cast(RuleConfig, self.get_rule(rule))
196
+
197
+ if attach:
198
+ if not rule.asset_names:
199
+ rule.asset_names = asset_names
200
+ else:
201
+ rule.asset_names.extend(asset_names)
202
+ else:
203
+ rule.asset_names = list(set(rule.asset_names) ^ set(asset_names))
204
+
205
+ if not rule.asset_names:
206
+ raise ValueError(f"Rule '{rule.name}' must be associated with at least one asset.")
207
+
208
+ req = self._update_req_from_rule_config(rule)
209
+ self._rule_service_stub.UpdateRule(req)
210
+
211
+ return rule
212
+
213
+ def create_or_update_rule(self, config: RuleConfig):
214
+ """
215
+ Create or update a rule via a RuleConfig. The config must contain a rule_client_key or an exception will be raised.
216
+ If a rule with the given client key already exists it will be updated, otherwise it will be created.
217
+ See `sift_py.rule.config.RuleConfig` for more information on configuation parameters for rules.
218
+ """
219
+ if not config.rule_client_key:
220
+ raise ValueError(f"Rule of name '{config.name}' requires a rule_client_key")
221
+
222
+ rule = self._get_rule_from_client_key(config.rule_client_key)
223
+ if rule:
224
+ self._update_rule(config, rule)
225
+ else:
226
+ self._create_rule(config)
227
+
228
+ def _update_rule(self, updated_config: RuleConfig, rule: Rule):
229
+ req = self._update_req_from_rule_config(updated_config, rule)
230
+ self._rule_service_stub.UpdateRule(req)
231
+
232
+ def _create_rule(self, config: RuleConfig):
233
+ req = self._update_req_from_rule_config(config)
234
+ self._rule_service_stub.CreateRule(CreateRuleRequest(update=req))
235
+
236
+ def _update_req_from_rule(self, rule: Rule) -> UpdateRuleRequest:
237
+ return UpdateRuleRequest(
238
+ organization_id=rule.organization_id,
239
+ rule_id=rule.rule_id,
240
+ client_key=rule.client_key,
241
+ name=rule.name,
242
+ description=rule.description,
243
+ conditions=[
244
+ UpdateConditionRequest(
245
+ rule_condition_id=condition.rule_condition_id,
246
+ actions=[
247
+ UpdateActionRequest(
248
+ rule_action_id=action.rule_action_id,
249
+ action_type=action.action_type,
250
+ configuration=action.configuration,
251
+ )
252
+ for action in condition.actions
253
+ ],
254
+ expression=condition.expression,
255
+ )
256
+ for condition in rule.conditions
257
+ ],
258
+ asset_configuration=RuleAssetConfiguration(
259
+ asset_ids=rule.asset_configuration.asset_ids,
260
+ ),
261
+ )
262
+
263
+ def _update_req_from_rule_config(
264
+ self, config: RuleConfig, rule: Optional[Rule] = None
265
+ ) -> UpdateRuleRequest:
266
+ if not config.expression:
267
+ raise ValueError(
268
+ "Cannot create a rule with an empty expression."
269
+ "See `sift_py.rule.config.RuleConfig` for more information on rule configuration."
270
+ )
271
+
272
+ if not config.action:
273
+ raise ValueError(
274
+ "Cannot create a rule with no corresponding action."
275
+ "See `sift_py.rule.config.RuleAction` for available actions."
276
+ )
277
+
278
+ # TODO:
279
+ # - once we have TagService_ListTags we can do asset-agnostic rules via tags
280
+ assets = self._get_assets(names=config.asset_names) if config.asset_names else None
281
+
282
+ actions = []
283
+ if config.action.kind() == RuleActionKind.NOTIFICATION:
284
+ raise NotImplementedError(
285
+ "Notification actions are not yet supported."
286
+ "Please contact the Sift team for assistance."
287
+ )
288
+ elif config.action.kind() == RuleActionKind.ANNOTATION:
289
+ if isinstance(config.action, RuleActionCreateDataReviewAnnotation):
290
+ assignee = config.action.assignee
291
+ user_id = None
292
+ if assignee:
293
+ users = get_active_users(
294
+ user_service=self._user_service_stub,
295
+ filter=f"name=='{assignee}'",
296
+ )
297
+ if not users:
298
+ raise ValueError(f"Cannot find user '{assignee}'.")
299
+ if len(users) > 1:
300
+ raise ValueError(f"Multiple users found with name '{assignee}'.")
301
+ user_id = users[0].user_id
302
+
303
+ action_config = UpdateActionRequest(
304
+ action_type=ANNOTATION,
305
+ configuration=RuleActionConfiguration(
306
+ annotation=AnnotationActionConfiguration(
307
+ assigned_to_user_id=user_id,
308
+ # tag_ids=config.action.tags, # TODO: Requires TagService
309
+ )
310
+ ),
311
+ )
312
+ actions.append(action_config)
313
+ elif isinstance(config.action, RuleActionCreatePhaseAnnotation):
314
+ action_config = UpdateActionRequest(
315
+ action_type=ANNOTATION,
316
+ configuration=RuleActionConfiguration(
317
+ annotation=AnnotationActionConfiguration(
318
+ # tag_ids=config.action.tags, # TODO: Requires TagService
319
+ )
320
+ ),
321
+ )
322
+
323
+ channel_references = {}
324
+ for channel_reference in config.channel_references:
325
+ ref = channel_reference["channel_reference"]
326
+ ident = channel_reference_from_fqn(channel_reference["channel_identifier"])
327
+ channel_references[ref] = ident
328
+
329
+ if assets and channel_references:
330
+ identifiers = [ident.name for ident in channel_references.values()]
331
+ components = [ident.component for ident in channel_references.values()]
332
+
333
+ # Create CEL search filters
334
+ name_in = cel_in("name", identifiers)
335
+ component_in = cel_in("component", components)
336
+
337
+ # Validate channels are present within each asset
338
+ for asset in assets:
339
+ found_channels = get_channels(
340
+ channel_service=self._channel_service_stub,
341
+ filter=f"asset_id == '{asset.asset_id}' && {name_in} && {component_in}",
342
+ )
343
+ found_channels_names = [channel.name for channel in found_channels]
344
+
345
+ missing_channels = set(identifiers) ^ set(found_channels_names)
346
+ if missing_channels:
347
+ raise RuntimeError(
348
+ f"Asset {asset.name} is missing channels required for rule {config.name}: {missing_channels}"
349
+ )
350
+
351
+ rule_id = None
352
+ organization_id = ""
353
+ if rule:
354
+ rule_id = rule.rule_id
355
+ organization_id = rule.organization_id
356
+
357
+ return UpdateRuleRequest(
358
+ organization_id=organization_id,
359
+ rule_id=rule_id,
360
+ client_key=config.rule_client_key,
361
+ name=config.name,
362
+ description=config.description,
363
+ conditions=[
364
+ UpdateConditionRequest(
365
+ actions=actions,
366
+ expression=RuleConditionExpression(
367
+ calculated_channel=CalculatedChannelConfig(
368
+ expression=config.expression,
369
+ channel_references=channel_references,
370
+ )
371
+ ),
372
+ )
373
+ ],
374
+ asset_configuration=RuleAssetConfiguration(
375
+ asset_ids=[asset.asset_id for asset in assets] if assets else None,
376
+ ),
377
+ )
378
+
379
+ def get_rule(self, rule: str) -> Optional[RuleConfig]:
380
+ """
381
+ Get a rule by rule id or client key. Returns a RuleConfig if the rule exists, otherwise None.
382
+ """
383
+ rule_pb = self._get_rule_from_client_key(rule) or self._get_rule_from_rule_id(rule)
384
+ if not rule_pb:
385
+ return None
386
+
387
+ channel_references: List[ExpressionChannelReference] = []
388
+ expression = ""
389
+ action: Optional[
390
+ Union[RuleActionCreateDataReviewAnnotation, RuleActionCreatePhaseAnnotation]
391
+ ] = None
392
+ for condition in rule_pb.conditions:
393
+ expression = condition.expression.calculated_channel.expression
394
+ for ref, id in condition.expression.calculated_channel.channel_references.items():
395
+ channel_references.append(
396
+ {
397
+ "channel_reference": ref,
398
+ "channel_identifier": id.name,
399
+ }
400
+ )
401
+ for action_config in condition.actions:
402
+ annotation_type = action_config.configuration.annotation.annotation_type
403
+ if annotation_type == AnnotationType.ANNOTATION_TYPE_PHASE:
404
+ action = RuleActionCreatePhaseAnnotation(
405
+ tags=[tag for tag in action_config.configuration.annotation.tag_ids],
406
+ )
407
+ else:
408
+ assignee = action_config.configuration.annotation.assigned_to_user_id
409
+ action = RuleActionCreateDataReviewAnnotation(
410
+ assignee=assignee,
411
+ tags=[tag for tag in action_config.configuration.annotation.tag_ids],
412
+ )
413
+
414
+ assets = self._get_assets(
415
+ ids=[asset_id for asset_id in rule_pb.asset_configuration.asset_ids]
416
+ )
417
+ asset_names = [asset.name for asset in assets]
418
+
419
+ rule_config = RuleConfig(
420
+ name=rule_pb.name,
421
+ description=rule_pb.description,
422
+ rule_client_key=rule_pb.client_key,
423
+ channel_references=channel_references, # type: ignore
424
+ asset_names=asset_names,
425
+ action=action,
426
+ expression=expression,
427
+ )
428
+
429
+ return rule_config
430
+
431
+ def _get_rule_from_client_key(self, client_key: str) -> Optional[Rule]:
432
+ req = GetRuleRequest(client_key=client_key)
433
+ try:
434
+ res = cast(GetRuleResponse, self._rule_service_stub.GetRule(req))
435
+ return res.rule or None
436
+ except:
437
+ return None
438
+
439
+ def _get_rule_from_rule_id(self, rule_id: str) -> Optional[Rule]:
440
+ req = GetRuleRequest(rule_id=rule_id)
441
+ try:
442
+ res = cast(GetRuleResponse, self._rule_service_stub.GetRule(req))
443
+ return res.rule or None
444
+ except:
445
+ return None
446
+
447
+ def _get_assets(self, names: List[str] = [], ids: List[str] = []) -> List[Asset]:
448
+ def get_assets_with_filter(cel_filter: str):
449
+ assets: List[Asset] = []
450
+ next_page_token = ""
451
+ while True:
452
+ req = ListAssetsRequest(
453
+ filter=cel_filter,
454
+ page_size=1_000,
455
+ page_token=next_page_token,
456
+ )
457
+ res = cast(ListAssetsResponse, self._asset_service_stub.ListAssets(req))
458
+ assets.extend(res.assets)
459
+
460
+ if not res.next_page_token:
461
+ break
462
+ next_page_token = res.next_page_token
463
+
464
+ return assets
465
+
466
+ if names:
467
+ names_cel = cel_in("name", names)
468
+ return get_assets_with_filter(names_cel)
469
+ elif ids:
470
+ ids_cel = cel_in("asset_id", ids)
471
+ return get_assets_with_filter(ids_cel)
472
+ else:
473
+ return []
474
+
475
+
476
+ @dataclass
477
+ class RuleChannelReference:
478
+ """
479
+ Convenient wrapper to map rule names to relevant channel references
480
+ when creating rules from yaml.
481
+ """
482
+
483
+ rule_name: str
484
+ channel_references: Dict[str, Any]
File without changes
@@ -0,0 +1,169 @@
1
+ import pytest
2
+
3
+ from sift_py.ingestion.config.yaml.error import YamlConfigError
4
+ from sift_py.yaml import channel
5
+
6
+
7
+ def test__validate_channel_anchor():
8
+ channel._validate_channel_anchor("foo")
9
+
10
+ with pytest.raises(YamlConfigError, match="Expected '<str>'"):
11
+ channel._validate_channel_anchor(3)
12
+
13
+
14
+ def test__validate_enum_type():
15
+ channel._validate_enum_type(
16
+ {
17
+ "name": "foo",
18
+ "key": 0,
19
+ }
20
+ )
21
+
22
+ with pytest.raises(YamlConfigError, match="Expected 'key' to be <int> but it is <str>"):
23
+ channel._validate_enum_type(
24
+ {
25
+ "name": "foo",
26
+ "key": "foobar",
27
+ }
28
+ )
29
+
30
+ with pytest.raises(YamlConfigError, match="Expected 'name' to be <str> but it is <int>"):
31
+ channel._validate_enum_type(
32
+ {
33
+ "name": 123,
34
+ "key": 0,
35
+ }
36
+ )
37
+
38
+
39
+ def test__validate_bit_field_element():
40
+ channel._validate_bit_field_element(
41
+ {
42
+ "name": "heater",
43
+ "index": 0,
44
+ "bit_count": 3,
45
+ }
46
+ )
47
+
48
+ with pytest.raises(YamlConfigError, match="Expected 'name' to be <str> but it is <int>"):
49
+ channel._validate_bit_field_element(
50
+ {
51
+ "name": 0,
52
+ "index": 0,
53
+ "bit_count": 3,
54
+ }
55
+ )
56
+
57
+ with pytest.raises(YamlConfigError, match="Expected 'index' to be <int> but it is <str>"):
58
+ channel._validate_bit_field_element(
59
+ {
60
+ "name": "heater",
61
+ "index": "foobar",
62
+ "bit_count": 3,
63
+ }
64
+ )
65
+
66
+ with pytest.raises(YamlConfigError, match="Expected 'bit_count' to be <int> but it is <str>"):
67
+ channel._validate_bit_field_element(
68
+ {
69
+ "name": "heater",
70
+ "index": 0,
71
+ "bit_count": "foo",
72
+ }
73
+ )
74
+
75
+
76
+ def test__validate_channel():
77
+ channel._validate_channel(
78
+ {
79
+ "name": "force",
80
+ "data_type": "double",
81
+ "unit": "N",
82
+ }
83
+ )
84
+
85
+ with pytest.raises(YamlConfigError, match="Expected 'data_type' to be"):
86
+ channel._validate_channel(
87
+ {
88
+ "name": "force",
89
+ "data_type": "dubble",
90
+ }
91
+ )
92
+
93
+ with pytest.raises(YamlConfigError, match="Expected 'description' to be <str> but it is <int>"):
94
+ channel._validate_channel(
95
+ {
96
+ "name": "force",
97
+ "description": 9001,
98
+ "data_type": "double",
99
+ "unit": "N",
100
+ }
101
+ )
102
+
103
+ with pytest.raises(YamlConfigError, match="Expected 'unit' to be <str> but it is <list>"):
104
+ channel._validate_channel(
105
+ {
106
+ "name": "force",
107
+ "description": "use the force",
108
+ "data_type": "double",
109
+ "component": "motor",
110
+ "unit": [1, 2, 3],
111
+ }
112
+ )
113
+
114
+ with pytest.raises(YamlConfigError, match="Expected 'component' to be <str> but it is <dict>"):
115
+ channel._validate_channel(
116
+ {
117
+ "name": "force",
118
+ "description": "use the force",
119
+ "data_type": "double",
120
+ "component": {},
121
+ "unit": "N",
122
+ }
123
+ )
124
+
125
+ with pytest.raises(YamlConfigError, match="should not have 'bit_field_elements' set"):
126
+ channel._validate_channel(
127
+ {
128
+ "name": "force",
129
+ "description": "use the force",
130
+ "data_type": "double",
131
+ "component": "motor",
132
+ "unit": "N",
133
+ "bit_field_elements": [{"name": "heat", "index": 0, "bit_count": 3}],
134
+ }
135
+ )
136
+
137
+ channel._validate_channel(
138
+ {
139
+ "name": "force",
140
+ "description": "use the force",
141
+ "data_type": "bit_field",
142
+ "component": "motor",
143
+ "unit": "N",
144
+ "bit_field_elements": [{"name": "heat", "index": 0, "bit_count": 3}],
145
+ }
146
+ )
147
+
148
+ with pytest.raises(YamlConfigError, match="should not have 'enum_types' set"):
149
+ channel._validate_channel(
150
+ {
151
+ "name": "force",
152
+ "description": "use the force",
153
+ "data_type": "double",
154
+ "component": "motor",
155
+ "unit": "N",
156
+ "enum_types": [{"name": "heat", "key": 0}],
157
+ }
158
+ )
159
+
160
+ channel._validate_channel(
161
+ {
162
+ "name": "force",
163
+ "description": "use the force",
164
+ "data_type": "enum",
165
+ "component": "motor",
166
+ "unit": "N",
167
+ "enum_types": [{"name": "heat", "key": 0}],
168
+ }
169
+ )