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,350 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from datetime import datetime
5
+ from typing import Dict, List, Optional
6
+ from warnings import warn
7
+
8
+ from google.protobuf.timestamp_pb2 import Timestamp
9
+ from sift.ingest.v1.ingest_pb2 import (
10
+ IngestWithConfigDataChannelValue,
11
+ IngestWithConfigDataStreamRequest,
12
+ )
13
+ from sift.ingest.v1.ingest_pb2_grpc import IngestServiceStub
14
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import ChannelConfig as ChannelConfigPb
15
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import IngestionConfig
16
+
17
+ from sift_py.grpc.transport import SiftChannel
18
+ from sift_py.ingestion._internal.error import IngestionValidationError
19
+ from sift_py.ingestion._internal.ingestion_config import (
20
+ create_flow_configs,
21
+ create_ingestion_config,
22
+ get_ingestion_config_by_client_key,
23
+ get_ingestion_config_flows,
24
+ )
25
+ from sift_py.ingestion._internal.run import create_run, get_run_id_by_name
26
+ from sift_py.ingestion.channel import (
27
+ ChannelConfig,
28
+ ChannelValue,
29
+ channel_fqn,
30
+ empty_value,
31
+ is_data_type,
32
+ )
33
+ from sift_py.ingestion.config.telemetry import TelemetryConfig
34
+ from sift_py.ingestion.flow import Flow, FlowConfig, FlowOrderedChannelValues
35
+ from sift_py.ingestion.rule.config import RuleConfig
36
+ from sift_py.rule.service import RuleService
37
+
38
+
39
+ class _IngestionServiceImpl:
40
+ transport_channel: SiftChannel
41
+ ingestion_config: IngestionConfig
42
+ asset_name: str
43
+ flow_configs_by_name: Dict[str, FlowConfig]
44
+ rules: List[RuleConfig]
45
+ run_id: Optional[str]
46
+ organization_id: Optional[str]
47
+ end_stream_on_error: bool
48
+
49
+ ingest_service_stub: IngestServiceStub
50
+ rule_service: RuleService
51
+
52
+ def __init__(
53
+ self,
54
+ channel: SiftChannel,
55
+ config: TelemetryConfig,
56
+ run_id: Optional[str] = None,
57
+ overwrite_rules: bool = False,
58
+ end_stream_on_error: bool = False,
59
+ ):
60
+ ingestion_config = self.__class__._get_or_create_ingestion_config(channel, config)
61
+
62
+ if overwrite_rules:
63
+ warn(
64
+ "The 'overwrite_rules' argument is deprecated and will be removed in release 0.4.0."
65
+ )
66
+
67
+ self.rule_service = RuleService(channel)
68
+ if config.rules:
69
+ for rule in config.rules:
70
+ if config.asset_name not in rule.asset_names:
71
+ rule.asset_names.append(config.asset_name)
72
+ self.rule_service.create_or_update_rules(config.rules)
73
+
74
+ self.rules = config.rules
75
+ self.ingestion_config = ingestion_config
76
+ self.asset_name = config.asset_name
77
+ self.transport_channel = channel
78
+ self.run_id = run_id
79
+ self.organization_id = config.organization_id
80
+ self.end_stream_on_error = end_stream_on_error
81
+ self.flow_configs_by_name = {flow.name: flow for flow in config.flows}
82
+ self.ingest_service_stub = IngestServiceStub(channel)
83
+
84
+ def ingest(self, *requests: IngestWithConfigDataStreamRequest):
85
+ """
86
+ Perform data ingestion.
87
+ """
88
+ self.ingest_service_stub.IngestWithConfigDataStream(iter(requests))
89
+
90
+ def ingest_flows(self, *flows: FlowOrderedChannelValues):
91
+ """
92
+ Combines the requests creation step and ingestion into a single call.
93
+ See `create_ingestion_request` for information about how client-side validations are handled.
94
+ """
95
+
96
+ requests = []
97
+ for flow in flows:
98
+ flow_name = flow["flow_name"]
99
+ timestamp = flow["timestamp"]
100
+ channel_values = flow["channel_values"]
101
+ req = self.create_ingestion_request(flow_name, timestamp, channel_values)
102
+ requests.append(req)
103
+
104
+ self.ingest_service_stub.IngestWithConfigDataStream(iter(requests))
105
+
106
+ def try_ingest_flows(self, *flows: Flow):
107
+ """
108
+ Combines the requests creation step and ingestion into a single call.
109
+ See `try_create_ingestion_request` for information about how client-side validations are handled.
110
+ """
111
+
112
+ requests = []
113
+ for flow in flows:
114
+ flow_name = flow["flow_name"]
115
+ timestamp = flow["timestamp"]
116
+ channel_values = flow["channel_values"]
117
+ req = self.try_create_ingestion_request(flow_name, timestamp, channel_values)
118
+ requests.append(req)
119
+
120
+ self.ingest_service_stub.IngestWithConfigDataStream(iter(requests))
121
+
122
+ def attach_run(
123
+ self,
124
+ channel: SiftChannel,
125
+ run_name: str,
126
+ description: Optional[str] = None,
127
+ organization_id: Optional[str] = None,
128
+ tags: Optional[List[str]] = None,
129
+ ):
130
+ """
131
+ Retrieve an existing run or create one to use during this period of ingestion.
132
+ """
133
+ run_id = get_run_id_by_name(channel, run_name)
134
+
135
+ if run_id is not None:
136
+ self.run_id = run_id
137
+ return
138
+
139
+ self.run_id = create_run(
140
+ channel=channel,
141
+ run_name=run_name,
142
+ description=description or "",
143
+ organization_id=organization_id or "",
144
+ tags=tags or [],
145
+ )
146
+
147
+ def detach_run(self):
148
+ """
149
+ Detach run from this period of ingestion. Subsequent data ingested won't be associated with
150
+ the run being detached.
151
+ """
152
+ self.run_id = None
153
+
154
+ def try_create_ingestion_request(
155
+ self,
156
+ flow_name: str,
157
+ timestamp: datetime,
158
+ channel_values: List[ChannelValue],
159
+ ) -> IngestWithConfigDataStreamRequest:
160
+ """
161
+ Creates an ingestion request for a flow that must exist in `flow_configs_by_name`. This method
162
+ performs a series of client-side validations and will return a `IngestionValidationError` if any validations fail.
163
+ """
164
+ flow_config = self.flow_configs_by_name.get(flow_name)
165
+
166
+ if flow_config is None:
167
+ raise IngestionValidationError(
168
+ f"A flow config of name '{flow_name}' could not be found."
169
+ )
170
+
171
+ channel_values_by_fqn: Dict[str, ChannelValue] = {}
172
+
173
+ for channel_value in channel_values:
174
+ fqn = channel_fqn(channel_value)
175
+
176
+ if channel_values_by_fqn.get(fqn, None) is None:
177
+ channel_values_by_fqn[fqn] = channel_value
178
+ else:
179
+ raise IngestionValidationError(f"Encountered multiple values for {fqn}")
180
+
181
+ values: List[IngestWithConfigDataChannelValue] = []
182
+
183
+ for channel in flow_config.channels:
184
+ fqn = channel_fqn(channel)
185
+ channel_val: Optional[ChannelValue] = channel_values_by_fqn.pop(fqn, None)
186
+
187
+ if channel_val is None:
188
+ values.append(empty_value())
189
+ continue
190
+
191
+ value = channel_val["value"]
192
+
193
+ if is_data_type(value, channel.data_type):
194
+ values.append(value)
195
+ else:
196
+ raise IngestionValidationError(
197
+ f"Expected value for `{channel.name}` to be a '{channel.data_type}'."
198
+ )
199
+
200
+ if len(channel_values_by_fqn) > 0:
201
+ unexpected_channels = [name for name in channel_values_by_fqn.keys()]
202
+ raise IngestionValidationError(
203
+ f"Unexpected channel(s) for flow '{flow_name}': {unexpected_channels}"
204
+ )
205
+
206
+ if timestamp.tzname() != "UTC":
207
+ raise IngestionValidationError(
208
+ f"Expected 'timestamp' to be in UTC but it is in {timestamp.tzname()}."
209
+ )
210
+
211
+ timestamp_pb = Timestamp()
212
+ timestamp_pb.FromDatetime(timestamp)
213
+
214
+ return self.create_ingestion_request(flow_name, timestamp, values)
215
+
216
+ def create_ingestion_request(
217
+ self,
218
+ flow_name: str,
219
+ timestamp: datetime,
220
+ channel_values: List[IngestWithConfigDataChannelValue],
221
+ ) -> IngestWithConfigDataStreamRequest:
222
+ """
223
+ Creates an ingestion request for a flow that must exist in `flow_configs_by_name`. This method
224
+ does not do any sort of client-side validation and is recommended to use if performance is required.
225
+ """
226
+ timestamp_pb = Timestamp()
227
+ timestamp_pb.FromDatetime(timestamp)
228
+
229
+ return IngestWithConfigDataStreamRequest(
230
+ ingestion_config_id=self.ingestion_config.ingestion_config_id,
231
+ flow=flow_name,
232
+ timestamp=timestamp_pb,
233
+ channel_values=channel_values,
234
+ run_id=self.run_id or "",
235
+ organization_id=self.organization_id or "",
236
+ end_stream_on_validation_error=self.end_stream_on_error,
237
+ )
238
+
239
+ def try_create_flow(self, flow_config: FlowConfig):
240
+ """
241
+ Tries to create a new flow at runtime. Will raise an `IngestionValidationError` if there already exists
242
+ a flow with the name of the `flow_config` argument. If you'd like to overwrite any flow configs with that
243
+ have the same name as the provided `flow_config`, then see `create_flow`.
244
+ """
245
+
246
+ if flow_config.name in self.flow_configs_by_name:
247
+ raise IngestionValidationError(
248
+ f"There is already a flow with name '{flow_config.name}'."
249
+ )
250
+
251
+ create_flow_configs(
252
+ self.transport_channel,
253
+ self.ingestion_config.ingestion_config_id,
254
+ [flow_config],
255
+ )
256
+
257
+ self.flow_configs_by_name[flow_config.name] = flow_config
258
+
259
+ def create_flow(self, flow_config: FlowConfig):
260
+ """
261
+ Like `try_create_flow` but will automatically overwrite any existing flow config with `flow_config` if they
262
+ share the same name. If you'd an exception to be raise in the case of a name collision then see `try_create_flow`.
263
+ """
264
+ create_flow_configs(
265
+ self.transport_channel,
266
+ self.ingestion_config.ingestion_config_id,
267
+ [flow_config],
268
+ )
269
+ self.flow_configs_by_name[flow_config.name] = flow_config
270
+
271
+ @staticmethod
272
+ def _update_flow_configs(
273
+ channel: SiftChannel, ingestion_config_id: str, telemetry_config: TelemetryConfig
274
+ ):
275
+ """
276
+ Compares local flows from a telemetry config with the flows registered in Sift. If a local flow
277
+ contains channels that isn't in Sift then this will create a new flow containing those channels.
278
+ """
279
+ config_flows = telemetry_config.flows
280
+
281
+ if len(config_flows) == 0:
282
+ return
283
+
284
+ sift_flows = get_ingestion_config_flows(channel, ingestion_config_id)
285
+
286
+ sift_flow_indices_by_name = {flow.name: i for i, flow in enumerate(sift_flows)}
287
+
288
+ flows_to_create: List[FlowConfig] = []
289
+
290
+ # We can have multiple channels of the same name but different data-type. This will create a completely unique channel
291
+ # identifier by creating a composite key of the fully qualified channel name with the channel's data-type.
292
+ sift_channel_identifier: Callable[[ChannelConfigPb], str] = (
293
+ lambda x: f"{channel_fqn(x)}.{x.data_type}"
294
+ )
295
+ config_channel_identifier: Callable[[ChannelConfig], str] = (
296
+ lambda x: f"{channel_fqn(x)}.{x.data_type.value}"
297
+ )
298
+
299
+ for config_flow in config_flows:
300
+ sift_flow_index = sift_flow_indices_by_name.get(config_flow.name)
301
+
302
+ # There isn't a flow in Sift for this config flow so we'll create it.
303
+ if sift_flow_index is None:
304
+ flows_to_create.append(config_flow)
305
+ continue
306
+
307
+ # There is a flow in Sift with the name of the config flow. We'll
308
+ # compare the channels in the config flow with the sift flow and
309
+ # see if there's a difference. If there is we'll create a new flow.
310
+ sift_flow = sift_flows[sift_flow_index]
311
+
312
+ sift_channel_identifiers = {
313
+ sift_channel_identifier(channel) for channel in sift_flow.channels
314
+ }
315
+
316
+ for config_channel in config_flow.channels:
317
+ # Found a channel for this flow that doesn't exist in Sift based on channel
318
+ # fully-qualified name and data-type. Create a new flow.
319
+ if not config_channel_identifier(config_channel) in sift_channel_identifiers:
320
+ flows_to_create.append(config_flow)
321
+ break
322
+
323
+ if len(flows_to_create) > 0:
324
+ create_flow_configs(channel, ingestion_config_id, flows_to_create)
325
+
326
+ @classmethod
327
+ def _get_or_create_ingestion_config(
328
+ cls, channel: SiftChannel, config: TelemetryConfig
329
+ ) -> IngestionConfig:
330
+ """
331
+ Retrieves an existing ingestion config or creates a new one. If an existing ingestion config is fetched,
332
+ then flows may be updated to reflect any changes that may have occured in the telemetry config.
333
+ """
334
+
335
+ ingestion_config = get_ingestion_config_by_client_key(channel, config.ingestion_client_key)
336
+
337
+ # Exiting ingestion config.. update flows if necessary
338
+ if ingestion_config is not None:
339
+ cls._update_flow_configs(channel, ingestion_config.ingestion_config_id, config)
340
+ return ingestion_config
341
+
342
+ ingestion_config = create_ingestion_config(
343
+ channel,
344
+ config.asset_name,
345
+ config.flows,
346
+ config.ingestion_client_key,
347
+ config.organization_id,
348
+ )
349
+
350
+ return ingestion_config
@@ -0,0 +1,357 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ import pytest
6
+ from pytest_mock import MockFixture
7
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import (
8
+ FlowConfig as FlowConfigPb,
9
+ )
10
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import (
11
+ IngestionConfig as IngestionConfigPb,
12
+ )
13
+
14
+ import sift_py.ingestion._internal.ingest
15
+ from sift_py._internal.test_util.channel import MockChannel
16
+ from sift_py._internal.test_util.fn import _mock_path as _mock_path_imp
17
+ from sift_py.ingestion._internal.error import IngestionValidationError
18
+ from sift_py.ingestion._internal.ingest import (
19
+ _IngestionServiceImpl,
20
+ )
21
+ from sift_py.ingestion._internal.ingestion_config import (
22
+ create_flow_configs,
23
+ create_ingestion_config,
24
+ get_ingestion_config_by_client_key,
25
+ get_ingestion_config_flows,
26
+ )
27
+ from sift_py.ingestion.channel import (
28
+ ChannelConfig,
29
+ ChannelDataType,
30
+ double_value,
31
+ int32_value,
32
+ string_value,
33
+ )
34
+ from sift_py.ingestion.config.telemetry import TelemetryConfig
35
+ from sift_py.ingestion.flow import FlowConfig
36
+ from sift_py.ingestion.rule.config import RuleActionCreateDataReviewAnnotation, RuleConfig
37
+
38
+ _mock_path = _mock_path_imp(sift_py.ingestion._internal.ingest)
39
+
40
+
41
+ def test_ingestion_service_update_flow_configs_updates_flows(mocker: MockFixture):
42
+ """
43
+ Tests to ensure that if a user adds a new flow to the telemetry config,
44
+ `update_flow_configs` will ensure that it gets created.
45
+ """
46
+ ingestion_config_id = "ingestion-config-id"
47
+
48
+ flow_a = FlowConfig(
49
+ name="flow_a",
50
+ channels=[
51
+ ChannelConfig(
52
+ name="channel_a",
53
+ data_type=ChannelDataType.DOUBLE,
54
+ ),
55
+ ],
56
+ )
57
+
58
+ flow_b = FlowConfig(
59
+ name="flow_b",
60
+ channels=[
61
+ ChannelConfig(
62
+ name="channel_b",
63
+ data_type=ChannelDataType.DOUBLE,
64
+ ),
65
+ ],
66
+ )
67
+
68
+ flows_from_api = [flow_a.as_pb(FlowConfigPb)]
69
+
70
+ telemetry_config = TelemetryConfig(
71
+ asset_name="my-config",
72
+ ingestion_client_key="my-key",
73
+ flows=[flow_a, flow_b],
74
+ )
75
+
76
+ mock_get_ingestion_config_flow_names = mocker.patch(_mock_path(get_ingestion_config_flows))
77
+ mock_get_ingestion_config_flow_names.return_value = flows_from_api
78
+
79
+ mock_create_flow_configs = mocker.patch(_mock_path(create_flow_configs))
80
+ mock_create_flow_configs.return_value = None
81
+
82
+ mock_channel = MockChannel()
83
+ _IngestionServiceImpl._update_flow_configs(mock_channel, ingestion_config_id, telemetry_config)
84
+ mock_create_flow_configs.assert_called_once_with(mock_channel, ingestion_config_id, [flow_b])
85
+
86
+
87
+ def test_ingestion_service_get_or_create_ingestion_config_retrieves_existing(mocker: MockFixture):
88
+ """
89
+ Ensure that if an ingestion config is queried by client key, a new one is not created.
90
+ """
91
+
92
+ mock_ingestion_config = IngestionConfigPb(
93
+ ingestion_config_id="ingestion-config-id",
94
+ asset_id="asset-id",
95
+ client_key="client-key",
96
+ )
97
+
98
+ mock_telemetry_config = TelemetryConfig(
99
+ asset_name="asset_name",
100
+ ingestion_client_key=mock_ingestion_config.client_key,
101
+ )
102
+
103
+ mock_get_ingestion_config_by_client_key = mocker.patch(
104
+ _mock_path(get_ingestion_config_by_client_key)
105
+ )
106
+ mock_get_ingestion_config_by_client_key.return_value = mock_ingestion_config
107
+
108
+ mock_create_ingestion_config = mocker.patch(_mock_path(create_ingestion_config))
109
+
110
+ mock_channel = MockChannel()
111
+
112
+ ingestion_config = _IngestionServiceImpl._get_or_create_ingestion_config(
113
+ mock_channel,
114
+ mock_telemetry_config,
115
+ )
116
+
117
+ mock_create_ingestion_config.assert_not_called()
118
+
119
+ assert ingestion_config.ingestion_config_id == mock_ingestion_config.ingestion_config_id
120
+
121
+
122
+ def test_ingestion_service_get_or_create_ingestion_config_create_if_not_exist(mocker: MockFixture):
123
+ """
124
+ Ensure that if an ingestion config does not exist for a given client key then a new
125
+ ingestion config is created based on the telemetry config.
126
+ """
127
+
128
+ mock_ingestion_config = IngestionConfigPb(
129
+ ingestion_config_id="ingestion-config-id",
130
+ asset_id="asset-id",
131
+ client_key="client-key",
132
+ )
133
+
134
+ mock_telemetry_config = TelemetryConfig(
135
+ asset_name="asset_name",
136
+ ingestion_client_key=mock_ingestion_config.client_key,
137
+ organization_id="my-org-id",
138
+ )
139
+
140
+ mock_get_ingestion_config_by_client_key = mocker.patch(
141
+ _mock_path(get_ingestion_config_by_client_key)
142
+ )
143
+ mock_get_ingestion_config_by_client_key.return_value = None
144
+
145
+ mock_create_ingestion_config = mocker.patch(_mock_path(create_ingestion_config))
146
+ mock_create_ingestion_config.return_value = mock_ingestion_config
147
+
148
+ mock_channel = MockChannel()
149
+
150
+ ingestion_config = _IngestionServiceImpl._get_or_create_ingestion_config(
151
+ mock_channel,
152
+ mock_telemetry_config,
153
+ )
154
+
155
+ mock_create_ingestion_config.assert_called_once_with(
156
+ mock_channel,
157
+ mock_telemetry_config.asset_name,
158
+ [],
159
+ mock_ingestion_config.client_key,
160
+ mock_telemetry_config.organization_id,
161
+ )
162
+
163
+ assert ingestion_config.ingestion_config_id == mock_ingestion_config.ingestion_config_id
164
+
165
+
166
+ def test_ingestion_service_try_create_ingestion_request_validations(mocker: MockFixture):
167
+ """
168
+ Tests all the different validations that happen when trying to create an ingestion request.
169
+ """
170
+
171
+ voltage_channel = ChannelConfig(
172
+ name="voltage",
173
+ component="motor",
174
+ data_type=ChannelDataType.DOUBLE,
175
+ )
176
+ pressure_channel = ChannelConfig(
177
+ name="pressure",
178
+ data_type=ChannelDataType.INT_64,
179
+ )
180
+ logs_channel = ChannelConfig(
181
+ name="logs",
182
+ data_type=ChannelDataType.STRING,
183
+ )
184
+
185
+ telemetry_config = TelemetryConfig(
186
+ asset_name="my-asset",
187
+ ingestion_client_key="my-client-key",
188
+ flows=[
189
+ FlowConfig(
190
+ name="reading",
191
+ channels=[voltage_channel, pressure_channel],
192
+ ),
193
+ FlowConfig(
194
+ name="pressure",
195
+ channels=[pressure_channel],
196
+ ),
197
+ FlowConfig(
198
+ name="log",
199
+ channels=[logs_channel],
200
+ ),
201
+ ],
202
+ )
203
+
204
+ mock_ingestion_config = IngestionConfigPb(
205
+ ingestion_config_id="ingestion-config-id",
206
+ asset_id="my-asset-id",
207
+ client_key="my-client-key",
208
+ )
209
+
210
+ mock_get_or_create_ingestion_config = mocker.patch.object(
211
+ _IngestionServiceImpl, "_get_or_create_ingestion_config"
212
+ )
213
+ mock_get_or_create_ingestion_config.return_value = mock_ingestion_config
214
+
215
+ mock_update_flow_configs = mocker.patch.object(_IngestionServiceImpl, "_update_flow_configs")
216
+ mock_update_flow_configs.return_value = None
217
+
218
+ transport_channel = MockChannel()
219
+
220
+ with mocker.patch("sift_py.ingestion._internal.ingest.RuleService"):
221
+ svc = _IngestionServiceImpl(
222
+ channel=transport_channel,
223
+ config=telemetry_config,
224
+ )
225
+
226
+ # Non-existent flow
227
+ with pytest.raises(IngestionValidationError, match="could not be found"):
228
+ svc.try_create_ingestion_request(
229
+ flow_name="lerg", # typo
230
+ timestamp=datetime.now(timezone.utc),
231
+ channel_values=[
232
+ {"channel_name": "logs", "value": string_value("foobar")},
233
+ ],
234
+ )
235
+
236
+ # Duplicate values for channel
237
+ with pytest.raises(IngestionValidationError, match="multiple values"):
238
+ svc.try_create_ingestion_request(
239
+ flow_name="log",
240
+ timestamp=datetime.now(timezone.utc),
241
+ channel_values=[
242
+ {"channel_name": "logs", "value": string_value("foobar")},
243
+ {"channel_name": "logs", "value": string_value("foobar")},
244
+ ],
245
+ )
246
+
247
+ # Wrong channel value type
248
+ with pytest.raises(IngestionValidationError, match="Expected value"):
249
+ svc.try_create_ingestion_request(
250
+ flow_name="log",
251
+ timestamp=datetime.now(timezone.utc),
252
+ channel_values=[
253
+ {"channel_name": "logs", "value": int32_value(32)},
254
+ ],
255
+ )
256
+
257
+ # Wrong channel for flow
258
+ with pytest.raises(IngestionValidationError, match="Unexpected channel"):
259
+ svc.try_create_ingestion_request(
260
+ flow_name="log",
261
+ timestamp=datetime.now(timezone.utc),
262
+ channel_values=[
263
+ {"channel_name": "voltage", "value": double_value(32)},
264
+ ],
265
+ )
266
+
267
+
268
+ def test_ingestion_service_init_with_rules(mocker: MockFixture):
269
+ """
270
+ Ensures that rules are created and updated to include the asset from the
271
+ telemetry config when the ingestion service is initialized.
272
+ """
273
+ voltage_channel = ChannelConfig(
274
+ name="voltage",
275
+ component="motor",
276
+ data_type=ChannelDataType.DOUBLE,
277
+ )
278
+ pressure_channel = ChannelConfig(
279
+ name="pressure",
280
+ data_type=ChannelDataType.INT_64,
281
+ )
282
+ logs_channel = ChannelConfig(
283
+ name="logs",
284
+ data_type=ChannelDataType.STRING,
285
+ )
286
+
287
+ rule_on_voltage = RuleConfig(
288
+ name="voltage_rule",
289
+ description="",
290
+ expression="$1 > 10",
291
+ channel_references=[
292
+ {"channel_reference": "$1", "channel_identifier": voltage_channel.fqn()},
293
+ ],
294
+ action=RuleActionCreateDataReviewAnnotation(
295
+ assignee="bob@example.com",
296
+ tags=["motor"],
297
+ ),
298
+ rule_client_key="voltage-rule-key",
299
+ )
300
+
301
+ rule_on_pressure = RuleConfig(
302
+ name="pressure_rule",
303
+ description="",
304
+ expression="$1 > 10",
305
+ channel_references=[
306
+ {"channel_reference": "$1", "channel_identifier": pressure_channel.fqn()},
307
+ ],
308
+ action=RuleActionCreateDataReviewAnnotation(
309
+ assignee="bob@example.com",
310
+ tags=["barometer"],
311
+ ),
312
+ rule_client_key="pressure-rule-key",
313
+ )
314
+
315
+ mock_ingestion_config = IngestionConfigPb(
316
+ ingestion_config_id="my-ingestion-config",
317
+ asset_id="my-asset-id",
318
+ client_key="my-client-key",
319
+ )
320
+
321
+ mock_get_or_create_ingestion_config = mocker.patch.object(
322
+ _IngestionServiceImpl, "_get_or_create_ingestion_config"
323
+ )
324
+ mock_get_or_create_ingestion_config.return_value = mock_ingestion_config
325
+
326
+ mock_update_flow_configs = mocker.patch.object(_IngestionServiceImpl, "_update_flow_configs")
327
+ mock_update_flow_configs.return_value = None
328
+
329
+ telemetry_config = TelemetryConfig(
330
+ asset_name="my-asset",
331
+ ingestion_client_key=mock_ingestion_config.client_key,
332
+ flows=[
333
+ FlowConfig(
334
+ name="reading",
335
+ channels=[voltage_channel, pressure_channel],
336
+ ),
337
+ FlowConfig(
338
+ name="pressure",
339
+ channels=[pressure_channel],
340
+ ),
341
+ FlowConfig(
342
+ name="log",
343
+ channels=[logs_channel],
344
+ ),
345
+ ],
346
+ rules=[rule_on_voltage, rule_on_pressure],
347
+ )
348
+
349
+ mock_channel = MockChannel()
350
+
351
+ with mocker.patch("sift_py.ingestion._internal.ingest.RuleService"):
352
+ svc = _IngestionServiceImpl(
353
+ channel=mock_channel,
354
+ config=telemetry_config,
355
+ )
356
+ for rule in svc.rules:
357
+ assert rule.asset_names == ["my-asset"]