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,130 @@
1
+ from typing import List, Optional, cast
2
+
3
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import (
4
+ CreateIngestionConfigFlowsRequest,
5
+ CreateIngestionConfigFlowsResponse,
6
+ CreateIngestionConfigRequest,
7
+ CreateIngestionConfigResponse,
8
+ IngestionConfig,
9
+ ListIngestionConfigFlowsRequest,
10
+ ListIngestionConfigFlowsResponse,
11
+ ListIngestionConfigsRequest,
12
+ ListIngestionConfigsResponse,
13
+ )
14
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import (
15
+ FlowConfig as FlowConfigPb,
16
+ )
17
+ from sift.ingestion_configs.v1.ingestion_configs_pb2_grpc import (
18
+ IngestionConfigServiceStub,
19
+ )
20
+
21
+ from sift_py.grpc.transport import SiftChannel
22
+ from sift_py.ingestion.flow import FlowConfig
23
+
24
+
25
+ def get_ingestion_config_by_client_key(
26
+ channel: SiftChannel,
27
+ client_key: str,
28
+ ) -> Optional[IngestionConfig]:
29
+ """
30
+ Returns `None` if no ingestion config can be matched with the provided `client_key`
31
+ """
32
+
33
+ svc = IngestionConfigServiceStub(channel)
34
+ req = ListIngestionConfigsRequest(
35
+ filter=f'client_key=="{client_key}"',
36
+ page_token="",
37
+ page_size=1,
38
+ )
39
+ res = cast(ListIngestionConfigsResponse, svc.ListIngestionConfigs(req))
40
+
41
+ if len(res.ingestion_configs) == 0:
42
+ return None
43
+ else:
44
+ return res.ingestion_configs[0]
45
+
46
+
47
+ def create_ingestion_config(
48
+ channel: SiftChannel,
49
+ asset_name: str,
50
+ flows: List[FlowConfig],
51
+ client_key: str,
52
+ organization_id: Optional[str],
53
+ ) -> IngestionConfig:
54
+ """
55
+ Creates a new ingestion config
56
+ """
57
+
58
+ svc = IngestionConfigServiceStub(channel)
59
+ req = CreateIngestionConfigRequest(
60
+ asset_name=asset_name,
61
+ client_key=client_key,
62
+ organization_id=organization_id or "",
63
+ flows=[flow.as_pb(FlowConfigPb) for flow in flows],
64
+ )
65
+ res = cast(CreateIngestionConfigResponse, svc.CreateIngestionConfig(req))
66
+ return res.ingestion_config
67
+
68
+
69
+ def get_ingestion_config_flow_names(
70
+ channel: SiftChannel,
71
+ ingestion_config_id: str,
72
+ ) -> List[str]:
73
+ """
74
+ Gets all names of flow configs of an ingestion config.
75
+ """
76
+ flows = get_ingestion_config_flows(channel, ingestion_config_id)
77
+ breakpoint()
78
+ return [flow.name for flow in flows]
79
+
80
+
81
+ def get_ingestion_config_flows(
82
+ channel: SiftChannel, ingestion_config_id: str
83
+ ) -> List[FlowConfigPb]:
84
+ svc = IngestionConfigServiceStub(channel)
85
+
86
+ flows: List[FlowConfigPb] = []
87
+
88
+ req = ListIngestionConfigFlowsRequest(
89
+ ingestion_config_id=ingestion_config_id,
90
+ page_size=1_000,
91
+ filter="",
92
+ )
93
+ res = cast(ListIngestionConfigFlowsResponse, svc.ListIngestionConfigFlows(req))
94
+
95
+ for flow in res.flows:
96
+ flows.append(flow)
97
+
98
+ page_token = res.next_page_token
99
+
100
+ while len(page_token) > 0:
101
+ req = ListIngestionConfigFlowsRequest(
102
+ ingestion_config_id=ingestion_config_id,
103
+ page_size=1_000,
104
+ filter="",
105
+ page_token=page_token,
106
+ )
107
+ res = cast(ListIngestionConfigFlowsResponse, svc.ListIngestionConfigFlows(req))
108
+
109
+ for flow in res.flows:
110
+ flows.append(flow)
111
+
112
+ page_token = res.next_page_token
113
+
114
+ return flows
115
+
116
+
117
+ def create_flow_configs(
118
+ channel: SiftChannel,
119
+ ingestion_config_id: str,
120
+ flow_configs: List[FlowConfig],
121
+ ):
122
+ """
123
+ Adds flow configs to an existing ingestion config.
124
+ """
125
+ svc = IngestionConfigServiceStub(channel)
126
+ req = CreateIngestionConfigFlowsRequest(
127
+ ingestion_config_id=ingestion_config_id,
128
+ flows=[f.as_pb(FlowConfigPb) for f in flow_configs],
129
+ )
130
+ _ = cast(CreateIngestionConfigFlowsResponse, svc.CreateIngestionConfigFlows(req))
@@ -0,0 +1,46 @@
1
+ from typing import List, Optional, cast
2
+
3
+ from sift.runs.v2.runs_pb2 import (
4
+ CreateRunRequest,
5
+ CreateRunResponse,
6
+ ListRunsRequest,
7
+ ListRunsResponse,
8
+ )
9
+ from sift.runs.v2.runs_pb2_grpc import RunServiceStub
10
+
11
+ from sift_py.grpc.transport import SiftChannel
12
+
13
+
14
+ def get_run_id_by_name(
15
+ channel: SiftChannel,
16
+ run_name: str,
17
+ ) -> Optional[str]:
18
+ svc = RunServiceStub(channel)
19
+ req = ListRunsRequest(
20
+ filter=f'name=="{run_name}"',
21
+ page_size=1,
22
+ )
23
+ res = cast(ListRunsResponse, svc.ListRuns(req))
24
+
25
+ if len(res.runs) == 0:
26
+ return None
27
+
28
+ return res.runs[0].run_id
29
+
30
+
31
+ def create_run(
32
+ channel: SiftChannel,
33
+ run_name: str,
34
+ description: str,
35
+ organization_id: str,
36
+ tags: List[str],
37
+ ) -> str:
38
+ svc = RunServiceStub(channel)
39
+ req = CreateRunRequest(
40
+ name=run_name,
41
+ description=description,
42
+ organization_id=organization_id,
43
+ tags=tags,
44
+ )
45
+ res = cast(CreateRunResponse, svc.CreateRun(req))
46
+ return res.run.run_id
@@ -0,0 +1,478 @@
1
+ import random
2
+ from contextlib import contextmanager
3
+ from datetime import datetime, timezone
4
+ from time import sleep
5
+ from typing import Callable, List
6
+
7
+ import pytest
8
+ from pytest_mock import MockFixture
9
+ from sift.ingest.v1.ingest_pb2 import IngestWithConfigDataStreamRequest
10
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import FlowConfig as FlowConfigPb
11
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import IngestionConfig as IngestionConfigPb
12
+
13
+ import sift_py.ingestion._internal.ingest
14
+ from sift_py._internal.test_util.channel import MockChannel
15
+ from sift_py._internal.test_util.fn import _mock_path as _mock_path_imp
16
+ from sift_py.ingestion._internal.error import IngestionValidationError
17
+ from sift_py.ingestion._internal.ingestion_config import (
18
+ create_flow_configs,
19
+ create_ingestion_config,
20
+ get_ingestion_config_by_client_key,
21
+ get_ingestion_config_flows,
22
+ )
23
+ from sift_py.ingestion.channel import ChannelConfig, ChannelDataType, double_value
24
+ from sift_py.ingestion.config.telemetry import TelemetryConfig
25
+ from sift_py.ingestion.flow import FlowConfig
26
+ from sift_py.ingestion.service import IngestionService
27
+
28
+ _mock_path = _mock_path_imp(sift_py.ingestion._internal.ingest)
29
+
30
+
31
+ def test_ingestion_service_buffered_ingestion(mocker: MockFixture):
32
+ """
33
+ Ensures that the ingestion method is being called the expected amount of times
34
+ when using the buffered method of ingestion.
35
+ """
36
+
37
+ mock_ingest = mocker.patch.object(IngestionService, "ingest")
38
+ mock_ingest.return_value = None
39
+
40
+ readings_flow = FlowConfig(
41
+ name="readings",
42
+ channels=[
43
+ ChannelConfig(
44
+ name="my-channel",
45
+ data_type=ChannelDataType.DOUBLE,
46
+ ),
47
+ ],
48
+ )
49
+
50
+ telemetry_config = TelemetryConfig(
51
+ asset_name="my-asset",
52
+ ingestion_client_key="ingestion-client-key",
53
+ flows=[readings_flow],
54
+ )
55
+
56
+ mock_ingestion_config = IngestionConfigPb(
57
+ ingestion_config_id="ingestion-config-id",
58
+ asset_id="asset-id",
59
+ client_key="client-key",
60
+ )
61
+
62
+ mock_get_ingestion_config_by_client_key = mocker.patch(
63
+ _mock_path(get_ingestion_config_by_client_key)
64
+ )
65
+ mock_get_ingestion_config_by_client_key.return_value = mock_ingestion_config
66
+
67
+ mock_get_ingestion_config_flows = mocker.patch(_mock_path(get_ingestion_config_flows))
68
+ mock_get_ingestion_config_flows.return_value = [readings_flow.as_pb(FlowConfigPb)]
69
+
70
+ ingestion_service = IngestionService(MockChannel(), telemetry_config)
71
+
72
+ @contextmanager
73
+ def mock_ctx_manager():
74
+ yield
75
+ mock_ingest.reset_mock()
76
+
77
+ with mock_ctx_manager():
78
+ with ingestion_service.buffered_ingestion() as buffered_ingestion:
79
+ assert buffered_ingestion._buffer_size == 1_000
80
+
81
+ for _ in range(10_000):
82
+ buffered_ingestion.try_ingest_flows(
83
+ {
84
+ "flow_name": "readings",
85
+ "timestamp": datetime.now(timezone.utc),
86
+ "channel_values": [
87
+ {"channel_name": "my-channel", "value": double_value(random.random())}
88
+ ],
89
+ }
90
+ )
91
+ assert mock_ingest.call_count == 10
92
+ assert len(buffered_ingestion._buffer) == 0
93
+
94
+ # No additional buffered items so no need for an extra ingest call
95
+ assert mock_ingest.call_count == 10
96
+
97
+ with mock_ctx_manager():
98
+ with ingestion_service.buffered_ingestion() as buffered_ingestion:
99
+ assert buffered_ingestion._buffer_size == 1_000
100
+
101
+ for _ in range(10_500):
102
+ buffered_ingestion.try_ingest_flows(
103
+ {
104
+ "flow_name": "readings",
105
+ "timestamp": datetime.now(timezone.utc),
106
+ "channel_values": [
107
+ {"channel_name": "my-channel", "value": double_value(random.random())}
108
+ ],
109
+ }
110
+ )
111
+
112
+ assert mock_ingest.call_count == 10
113
+ assert len(buffered_ingestion._buffer) == 500
114
+
115
+ # Exiting the context manager should call flush one more time
116
+ assert mock_ingest.call_count == 11
117
+
118
+ with mock_ctx_manager():
119
+ with ingestion_service.buffered_ingestion(500) as buffered_ingestion:
120
+ assert buffered_ingestion._buffer_size == 500
121
+
122
+ for _ in range(5_200):
123
+ buffered_ingestion.try_ingest_flows(
124
+ {
125
+ "flow_name": "readings",
126
+ "timestamp": datetime.now(timezone.utc),
127
+ "channel_values": [
128
+ {"channel_name": "my-channel", "value": double_value(random.random())}
129
+ ],
130
+ }
131
+ )
132
+
133
+ assert mock_ingest.call_count == 10
134
+ assert len(buffered_ingestion._buffer) == 200
135
+
136
+ assert mock_ingest.call_count == 11
137
+
138
+ with mock_ctx_manager():
139
+ with ingestion_service.buffered_ingestion(800) as buffered_ingestion:
140
+ for _ in range(5_200):
141
+ buffered_ingestion.ingest_flows(
142
+ {
143
+ "flow_name": "readings",
144
+ "timestamp": datetime.now(timezone.utc),
145
+ "channel_values": [double_value(random.random())],
146
+ }
147
+ )
148
+
149
+ assert mock_ingest.call_count == 6
150
+ assert len(buffered_ingestion._buffer) == 400
151
+
152
+ assert mock_ingest.call_count == 7
153
+
154
+ with mock_ctx_manager():
155
+ with ingestion_service.buffered_ingestion() as buffered_ingestion:
156
+ for _ in range(6_000):
157
+ buffered_ingestion.ingest_flows(
158
+ {
159
+ "flow_name": "readings",
160
+ "timestamp": datetime.now(timezone.utc),
161
+ "channel_values": [double_value(random.random())],
162
+ }
163
+ )
164
+
165
+ assert mock_ingest.call_count == 6
166
+ assert len(buffered_ingestion._buffer) == 0
167
+
168
+ assert mock_ingest.call_count == 6
169
+
170
+ with mock_ctx_manager():
171
+ with ingestion_service.buffered_ingestion() as buffered_ingestion:
172
+ for _ in range(6_600):
173
+ buffered_ingestion.ingest_flows(
174
+ {
175
+ "flow_name": "readings",
176
+ "timestamp": datetime.now(timezone.utc),
177
+ "channel_values": [double_value(random.random())],
178
+ }
179
+ )
180
+
181
+ assert mock_ingest.call_count == 6
182
+ assert len(buffered_ingestion._buffer) == 600
183
+
184
+ with pytest.raises(Exception):
185
+ raise
186
+
187
+ assert len(buffered_ingestion._buffer) == 0
188
+ assert mock_ingest.call_count == 7
189
+
190
+ with mock_ctx_manager():
191
+ on_error_spy = mocker.stub()
192
+
193
+ def on_error(
194
+ err: BaseException, requests: List[IngestWithConfigDataStreamRequest], _flush: Callable
195
+ ):
196
+ on_error_spy()
197
+ pass
198
+
199
+ with pytest.raises(Exception):
200
+ with ingestion_service.buffered_ingestion(on_error=on_error) as buffered_ingestion:
201
+ for _ in range(6_600):
202
+ buffered_ingestion.ingest_flows(
203
+ {
204
+ "flow_name": "readings",
205
+ "timestamp": datetime.now(timezone.utc),
206
+ "channel_values": [double_value(random.random())],
207
+ }
208
+ )
209
+ raise
210
+
211
+ on_error_spy.assert_called_once()
212
+ assert len(buffered_ingestion._buffer) == 600
213
+ assert mock_ingest.call_count == 6
214
+
215
+ with mock_ctx_manager():
216
+ on_error_flush_spy = mocker.stub()
217
+
218
+ def on_error(
219
+ err: BaseException, requests: List[IngestWithConfigDataStreamRequest], _flush: Callable
220
+ ):
221
+ on_error_flush_spy()
222
+ _flush()
223
+ pass
224
+
225
+ with pytest.raises(Exception):
226
+ with ingestion_service.buffered_ingestion(on_error=on_error) as buffered_ingestion:
227
+ for _ in range(6_600):
228
+ buffered_ingestion.ingest_flows(
229
+ {
230
+ "flow_name": "readings",
231
+ "timestamp": datetime.now(timezone.utc),
232
+ "channel_values": [double_value(random.random())],
233
+ }
234
+ )
235
+ raise
236
+
237
+ on_error_spy.assert_called_once()
238
+ assert len(buffered_ingestion._buffer) == 0
239
+ assert mock_ingest.call_count == 7
240
+
241
+
242
+ def test_ingestion_service_modify_existing_channel_configs(mocker: MockFixture):
243
+ """
244
+ Tests modifying existing channel configs in telemetry config. If a channel config
245
+ is modified in a telemetry config after it has already been used for ingestion
246
+ then we should create a new flow. If a user modifies a channel back to a previous
247
+ version (same component and name), then we should re-use an existing channel.
248
+ """
249
+
250
+ mock_ingestion_config = IngestionConfigPb(
251
+ ingestion_config_id="my-ingestion-config-id",
252
+ client_key="my-ingestion-config",
253
+ asset_id="my-asset-id",
254
+ )
255
+
256
+ channel_a = ChannelConfig(
257
+ name="channel_a",
258
+ component="A",
259
+ data_type=ChannelDataType.DOUBLE,
260
+ )
261
+
262
+ flow_a = FlowConfig(
263
+ name="flow_a",
264
+ channels=[channel_a],
265
+ )
266
+
267
+ telemetry_config = TelemetryConfig(
268
+ asset_name="my-asset-name",
269
+ ingestion_client_key=mock_ingestion_config.ingestion_config_id,
270
+ flows=[flow_a],
271
+ )
272
+
273
+ mock_get_ingestion_config_by_client_key = mocker.patch(
274
+ _mock_path(get_ingestion_config_by_client_key)
275
+ )
276
+ mock_get_ingestion_config_by_client_key.return_value = None
277
+
278
+ mock_create_ingestion_config = mocker.patch(_mock_path(create_ingestion_config))
279
+ mock_create_ingestion_config.return_value = mock_ingestion_config
280
+
281
+ mock_get_ingestion_config_flows = mocker.patch(_mock_path(get_ingestion_config_flows))
282
+ mock_get_ingestion_config_flows.return_value = [flow_a.as_pb(FlowConfigPb)]
283
+
284
+ mock_channel = MockChannel()
285
+
286
+ ingestion_service = IngestionService(
287
+ channel=mock_channel,
288
+ config=telemetry_config,
289
+ )
290
+
291
+ mock_create_ingestion_config.assert_called_once_with(
292
+ mock_channel,
293
+ telemetry_config.asset_name,
294
+ telemetry_config.flows,
295
+ telemetry_config.ingestion_client_key,
296
+ None,
297
+ )
298
+ assert ingestion_service.flow_configs_by_name[flow_a.name].channels[0] == channel_a
299
+
300
+ # Modify an existing channel but don't modify flow
301
+ channel_a.data_type = ChannelDataType.STRING
302
+
303
+ mock_create_flow_configs = mocker.patch(_mock_path(create_flow_configs))
304
+ mock_create_flow_configs.return_value = None
305
+
306
+ mock_get_ingestion_config_by_client_key.reset_mock()
307
+ mock_get_ingestion_config_by_client_key.return_value = mock_ingestion_config
308
+
309
+ # Re-initialize ingestion service
310
+ ingestion_service = IngestionService(
311
+ channel=mock_channel,
312
+ config=telemetry_config,
313
+ )
314
+
315
+ # Assert that we are trying to create a new flow with the same name as `flow_a`
316
+ # but with a new channel.
317
+ mock_create_flow_configs.assert_called_once_with(
318
+ mock_channel, mock_ingestion_config.ingestion_config_id, [flow_a]
319
+ )
320
+ assert ingestion_service.flow_configs_by_name[flow_a.name].channels[0] == channel_a
321
+
322
+ # Okay now what happens if someone were to change the channel config back to the original..
323
+
324
+ # Modify back to original
325
+ channel_a.data_type = ChannelDataType.DOUBLE
326
+
327
+ mock_create_flow_configs.reset_mock()
328
+
329
+ # Re-initialize ingestion service
330
+ ingestion_service = IngestionService(
331
+ channel=mock_channel,
332
+ config=telemetry_config,
333
+ )
334
+
335
+ # We shouldn't be creating a new flow, should re-use an existing flow.
336
+ mock_create_flow_configs.assert_not_called()
337
+ assert ingestion_service.flow_configs_by_name[flow_a.name].channels[0] == channel_a
338
+
339
+
340
+ def test_ingestion_service_register_new_flow(mocker: MockFixture):
341
+ mock_ingestion_config = IngestionConfigPb(
342
+ ingestion_config_id="my-ingestion-config-id",
343
+ client_key="my-ingestion-config",
344
+ asset_id="my-asset-id",
345
+ )
346
+
347
+ channel_a = ChannelConfig(
348
+ name="channel_a",
349
+ component="A",
350
+ data_type=ChannelDataType.DOUBLE,
351
+ )
352
+
353
+ flow_a = FlowConfig(
354
+ name="flow_a",
355
+ channels=[channel_a],
356
+ )
357
+
358
+ telemetry_config = TelemetryConfig(
359
+ asset_name="my-asset-name",
360
+ ingestion_client_key=mock_ingestion_config.ingestion_config_id,
361
+ flows=[flow_a],
362
+ )
363
+
364
+ mock_get_ingestion_config_by_client_key = mocker.patch(
365
+ _mock_path(get_ingestion_config_by_client_key)
366
+ )
367
+ mock_get_ingestion_config_by_client_key.return_value = None
368
+
369
+ mock_create_ingestion_config = mocker.patch(_mock_path(create_ingestion_config))
370
+ mock_create_ingestion_config.return_value = mock_ingestion_config
371
+
372
+ mock_get_ingestion_config_flows = mocker.patch(_mock_path(get_ingestion_config_flows))
373
+ mock_get_ingestion_config_flows.return_value = [flow_a.as_pb(FlowConfigPb)]
374
+
375
+ mock_channel = MockChannel()
376
+
377
+ ingestion_service = IngestionService(
378
+ channel=mock_channel,
379
+ config=telemetry_config,
380
+ )
381
+
382
+ new_flow_config = FlowConfig(
383
+ name="my_new_flow", channels=[ChannelConfig("new_channel", ChannelDataType.DOUBLE)]
384
+ )
385
+
386
+ mock_create_flow_configs = mocker.patch(_mock_path(create_flow_configs))
387
+ mock_create_flow_configs.return_value = None
388
+
389
+ assert ingestion_service.flow_configs_by_name.get("my_new_flow") is None
390
+
391
+ ingestion_service.try_create_flow(new_flow_config)
392
+
393
+ mock_create_flow_configs.assert_called_once_with(
394
+ mock_channel, mock_ingestion_config.ingestion_config_id, [new_flow_config]
395
+ )
396
+ assert ingestion_service.flow_configs_by_name["my_new_flow"] == new_flow_config
397
+
398
+ # Test the name collision
399
+ new_flow_config_name_collision = FlowConfig(
400
+ name="my_new_flow", channels=[ChannelConfig("foobar", ChannelDataType.DOUBLE)]
401
+ )
402
+
403
+ with pytest.raises(IngestionValidationError):
404
+ ingestion_service.try_create_flow(new_flow_config_name_collision)
405
+
406
+ # Bypass the validation
407
+ ingestion_service.create_flow(new_flow_config_name_collision)
408
+ assert ingestion_service.flow_configs_by_name["my_new_flow"] == new_flow_config_name_collision
409
+ assert ingestion_service.flow_configs_by_name["my_new_flow"] != new_flow_config
410
+
411
+
412
+ def test_ingestion_service_buffered_ingestion_flush_timeout(mocker: MockFixture):
413
+ """
414
+ Test for timeout based flush mechanism in buffered ingestion. If buffer hasn't been flushed
415
+ after a certain time then the buffer will be automatically flushed.
416
+ """
417
+
418
+ mock_ingest = mocker.patch.object(IngestionService, "ingest")
419
+ mock_ingest.return_value = None
420
+
421
+ readings_flow = FlowConfig(
422
+ name="readings",
423
+ channels=[
424
+ ChannelConfig(
425
+ name="my-channel",
426
+ data_type=ChannelDataType.DOUBLE,
427
+ ),
428
+ ],
429
+ )
430
+
431
+ telemetry_config = TelemetryConfig(
432
+ asset_name="my-asset",
433
+ ingestion_client_key="ingestion-client-key",
434
+ flows=[readings_flow],
435
+ )
436
+
437
+ mock_ingestion_config = IngestionConfigPb(
438
+ ingestion_config_id="ingestion-config-id",
439
+ asset_id="asset-id",
440
+ client_key="client-key",
441
+ )
442
+
443
+ mock_get_ingestion_config_by_client_key = mocker.patch(
444
+ _mock_path(get_ingestion_config_by_client_key)
445
+ )
446
+ mock_get_ingestion_config_by_client_key.return_value = mock_ingestion_config
447
+
448
+ mock_get_ingestion_config_flows = mocker.patch(_mock_path(get_ingestion_config_flows))
449
+ mock_get_ingestion_config_flows.return_value = [readings_flow.as_pb(FlowConfigPb)]
450
+
451
+ ingestion_service = IngestionService(MockChannel(), telemetry_config)
452
+
453
+ @contextmanager
454
+ def mock_ctx_manager():
455
+ yield
456
+ mock_ingest.reset_mock()
457
+
458
+ with mock_ctx_manager():
459
+ with ingestion_service.buffered_ingestion(flush_interval_sec=2) as buffered_ingestion:
460
+ assert buffered_ingestion._buffer_size == 1_000
461
+
462
+ for _ in range(1_500):
463
+ buffered_ingestion.try_ingest_flows(
464
+ {
465
+ "flow_name": "readings",
466
+ "timestamp": datetime.now(timezone.utc),
467
+ "channel_values": [
468
+ {"channel_name": "my-channel", "value": double_value(random.random())}
469
+ ],
470
+ }
471
+ )
472
+ assert mock_ingest.call_count == 1
473
+ assert len(buffered_ingestion._buffer) == 500
474
+
475
+ # This will cause the flush timer to flush based on provided interval
476
+ sleep(5)
477
+ assert mock_ingest.call_count == 2
478
+ assert len(buffered_ingestion._buffer) == 0