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,189 @@
1
+ import threading
2
+ from contextlib import contextmanager
3
+ from types import TracebackType
4
+ from typing import Callable, Generic, List, Optional, Type, TypeVar
5
+
6
+ from sift.ingest.v1.ingest_pb2 import IngestWithConfigDataStreamRequest
7
+ from typing_extensions import Self, TypeAlias
8
+
9
+ from sift_py.ingestion._internal.ingest import _IngestionServiceImpl
10
+ from sift_py.ingestion.flow import Flow, FlowOrderedChannelValues
11
+
12
+ DEFAULT_BUFFER_SIZE = 1_000
13
+
14
+ T = TypeVar("T", bound=_IngestionServiceImpl)
15
+
16
+ FlushCallback: TypeAlias = Callable[[], None]
17
+ OnErrorCallback: TypeAlias = Callable[
18
+ [BaseException, List[IngestWithConfigDataStreamRequest], FlushCallback], None
19
+ ]
20
+
21
+
22
+ class BufferedIngestionService(Generic[T]):
23
+ """
24
+ See `sift_py.ingestion.service.IngestionService.buffered_ingestion`
25
+ for more information and how to leverage buffered ingestion.
26
+ """
27
+
28
+ _buffer: List[IngestWithConfigDataStreamRequest]
29
+ _buffer_size: int
30
+ _ingestion_service: T
31
+ _flush_interval_sec: Optional[float]
32
+ _flush_timer: Optional[threading.Timer]
33
+ _lock: Optional[threading.Lock]
34
+ _on_error: Optional[OnErrorCallback]
35
+
36
+ def __init__(
37
+ self,
38
+ ingestion_service: T,
39
+ buffer_size: Optional[int],
40
+ flush_interval_sec: Optional[float],
41
+ on_error: Optional[OnErrorCallback],
42
+ ):
43
+ self._buffer = []
44
+ self._buffer_size = buffer_size or DEFAULT_BUFFER_SIZE
45
+ self._ingestion_service = ingestion_service
46
+ self._on_error = on_error
47
+ self._flush_timer = None
48
+
49
+ if flush_interval_sec:
50
+ self._flush_interval_sec = flush_interval_sec
51
+ self._lock = threading.Lock()
52
+ self._start_flush_timer()
53
+ else:
54
+ self._flush_interval_sec = None
55
+ self._lock = None
56
+
57
+ def __enter__(self) -> Self:
58
+ return self
59
+
60
+ def __exit__(
61
+ self,
62
+ exc_type: Optional[Type[BaseException]],
63
+ exc_val: Optional[BaseException],
64
+ exc_tb: Optional[TracebackType],
65
+ ) -> bool:
66
+ self._cancel_flush_timer()
67
+
68
+ if exc_val is not None:
69
+ if self._on_error is not None:
70
+ self._on_error(exc_val, self._buffer, self.flush)
71
+ else:
72
+ self.flush()
73
+
74
+ raise exc_val
75
+ else:
76
+ self.flush()
77
+
78
+ return True
79
+
80
+ def ingest_flows(self, *flows: FlowOrderedChannelValues):
81
+ """
82
+ Ingests flows in batches for each request generated from a flow.
83
+ See `sift_py.ingestion.service.IngestionService.create_ingestion_request`
84
+ for more information.
85
+ """
86
+ with self._use_lock():
87
+ lhs_cursor = 0
88
+ rhs_cursor = min(
89
+ self._buffer_size - len(self._buffer),
90
+ len(flows),
91
+ )
92
+
93
+ while lhs_cursor < len(flows):
94
+ for flow in flows[lhs_cursor:rhs_cursor]:
95
+ flow_name = flow["flow_name"]
96
+ timestamp = flow["timestamp"]
97
+ channel_values = flow["channel_values"]
98
+
99
+ req = self._ingestion_service.create_ingestion_request(
100
+ flow_name=flow_name,
101
+ timestamp=timestamp,
102
+ channel_values=channel_values,
103
+ )
104
+ self._buffer.append(req)
105
+
106
+ if len(self._buffer) >= self._buffer_size:
107
+ self._flush()
108
+
109
+ lhs_cursor = rhs_cursor
110
+ rhs_cursor = min(
111
+ rhs_cursor + (self._buffer_size - len(self._buffer)),
112
+ len(flows),
113
+ )
114
+
115
+ def try_ingest_flows(self, *flows: Flow):
116
+ """
117
+ Ingests flows in batches and performs client-side validations for each request
118
+ generated from a flow. See `sift_py.ingestion.service.IngestionService.try_create_ingestion_request`
119
+ for more information.
120
+ """
121
+ with self._use_lock():
122
+ lhs_cursor = 0
123
+ rhs_cursor = min(
124
+ self._buffer_size - len(self._buffer),
125
+ len(flows),
126
+ )
127
+
128
+ while lhs_cursor < len(flows):
129
+ for flow in flows[lhs_cursor:rhs_cursor]:
130
+ flow_name = flow["flow_name"]
131
+ timestamp = flow["timestamp"]
132
+ channel_values = flow["channel_values"]
133
+
134
+ req = self._ingestion_service.try_create_ingestion_request(
135
+ flow_name=flow_name,
136
+ timestamp=timestamp,
137
+ channel_values=channel_values,
138
+ )
139
+ self._buffer.append(req)
140
+
141
+ if len(self._buffer) >= self._buffer_size:
142
+ self._flush()
143
+
144
+ lhs_cursor = rhs_cursor
145
+ rhs_cursor = min(
146
+ rhs_cursor + (self._buffer_size - len(self._buffer)),
147
+ len(flows),
148
+ )
149
+
150
+ def flush(self):
151
+ """
152
+ Flush and ingest all requests in buffer.
153
+ """
154
+
155
+ if self._flush_timer and self._lock:
156
+ with self._lock:
157
+ self._flush()
158
+ self._restart_flush_timer()
159
+ else:
160
+ self._flush()
161
+
162
+ def _flush(self):
163
+ if len(self._buffer) > 0:
164
+ self._ingestion_service.ingest(*self._buffer)
165
+ self._buffer.clear()
166
+
167
+ def _start_flush_timer(self):
168
+ if self._flush_interval_sec:
169
+ self._flush_timer = threading.Timer(self._flush_interval_sec, self.flush)
170
+ self._flush_timer.start()
171
+
172
+ def _cancel_flush_timer(self):
173
+ if self._flush_timer:
174
+ self._flush_timer.cancel()
175
+ self._flush_timer = None
176
+
177
+ def _restart_flush_timer(self):
178
+ self._cancel_flush_timer()
179
+ self._start_flush_timer()
180
+
181
+ @contextmanager
182
+ def _use_lock(self):
183
+ try:
184
+ if self._lock:
185
+ self._lock.acquire()
186
+ yield
187
+ finally:
188
+ if self._lock:
189
+ self._lock.release()
@@ -0,0 +1,422 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import List, Optional, Type, TypedDict, Union
5
+
6
+ import sift.common.type.v1.channel_data_type_pb2 as channel_pb
7
+ from google.protobuf.empty_pb2 import Empty
8
+ from sift.channels.v2.channels_pb2 import Channel as ChannelPb
9
+ from sift.common.type.v1.channel_bit_field_element_pb2 import (
10
+ ChannelBitFieldElement as ChannelBitFieldElementPb,
11
+ )
12
+ from sift.common.type.v1.channel_enum_type_pb2 import (
13
+ ChannelEnumType as ChannelEnumTypePb,
14
+ )
15
+ from sift.ingest.v1.ingest_pb2 import IngestWithConfigDataChannelValue
16
+ from sift.ingestion_configs.v1.ingestion_configs_pb2 import ChannelConfig as ChannelConfigPb
17
+ from typing_extensions import NotRequired, Self
18
+
19
+ from sift_py._internal.channel import channel_fqn as _channel_fqn
20
+ from sift_py._internal.convert.protobuf import AsProtobuf
21
+
22
+
23
+ class ChannelValue(TypedDict):
24
+ """
25
+ Represents a fully qualified data point for a channel
26
+ """
27
+
28
+ channel_name: str
29
+ component: NotRequired[str]
30
+ value: IngestWithConfigDataChannelValue
31
+
32
+
33
+ class ChannelConfig(AsProtobuf):
34
+ """
35
+ A description for a channel
36
+ """
37
+
38
+ name: str
39
+ data_type: ChannelDataType
40
+ description: Optional[str]
41
+ unit: Optional[str]
42
+ component: Optional[str]
43
+ bit_field_elements: List[ChannelBitFieldElement]
44
+ enum_types: List[ChannelEnumType]
45
+ identifier: str
46
+
47
+ def __init__(
48
+ self,
49
+ name: str,
50
+ data_type: ChannelDataType,
51
+ description: Optional[str] = None,
52
+ unit: Optional[str] = None,
53
+ component: Optional[str] = None,
54
+ bit_field_elements: List[ChannelBitFieldElement] = [],
55
+ enum_types: List[ChannelEnumType] = [],
56
+ ):
57
+ self.name = name
58
+ self.data_type = data_type
59
+ self.description = description
60
+ self.unit = unit
61
+ self.component = component
62
+ self.bit_field_elements = bit_field_elements
63
+ self.enum_types = enum_types
64
+ self.identifier = self.fqn()
65
+
66
+ def value_from(
67
+ self, value: Optional[Union[int, float, bool, str]]
68
+ ) -> Optional[IngestWithConfigDataChannelValue]:
69
+ """
70
+ Like `try_value_from` except will return `None` there is a failure to produce a channel value due to a type mismatch.
71
+ """
72
+ try:
73
+ return self.try_value_from(value)
74
+ except ValueError:
75
+ return None
76
+
77
+ def try_value_from(
78
+ self, value: Optional[Union[int, float, bool, str]]
79
+ ) -> IngestWithConfigDataChannelValue:
80
+ """
81
+ Generate a channel value for this particular channel configuration. This will raise an exception
82
+ if there is a type match, namely, if `value` isn't consistent with the channel's data-type. For a version
83
+ of this function that does not raise an exception and simply ignores type mistmatches, see `value_from`. If `value`
84
+ is `None` then an empty value will be generated.
85
+ """
86
+ if value is None:
87
+ return empty_value()
88
+
89
+ if isinstance(value, int) or isinstance(value, float):
90
+ if self.data_type == ChannelDataType.INT_32:
91
+ return int32_value(int(value))
92
+ elif self.data_type == ChannelDataType.INT_64:
93
+ return int64_value(int(value))
94
+ elif self.data_type == ChannelDataType.UINT_32:
95
+ return uint32_value(int(value))
96
+ elif self.data_type == ChannelDataType.UINT_64:
97
+ return uint64_value(int(value))
98
+ elif self.data_type == ChannelDataType.FLOAT:
99
+ return float_value(float(value))
100
+ elif self.data_type == ChannelDataType.DOUBLE:
101
+ return double_value(float(value))
102
+ elif self.data_type == ChannelDataType.ENUM:
103
+ return enum_value(int(value))
104
+ elif isinstance(value, str) and self.data_type == ChannelDataType.STRING:
105
+ return string_value(value)
106
+ elif isinstance(value, bool) and self.data_type == ChannelDataType.BOOL:
107
+ return bool_value(value)
108
+
109
+ raise ValueError(f"Failed to cast value of type {type(value)} to {self.data_type}")
110
+
111
+ def as_pb(self, klass: Type[ChannelConfigPb]) -> ChannelConfigPb:
112
+ return klass(
113
+ name=self.name,
114
+ component=self.component or "",
115
+ unit=self.unit or "",
116
+ description=self.description or "",
117
+ data_type=self.data_type.value,
118
+ enum_types=[etype.as_pb(ChannelEnumTypePb) for etype in self.enum_types],
119
+ bit_field_elements=[
120
+ el.as_pb(ChannelBitFieldElementPb) for el in self.bit_field_elements
121
+ ],
122
+ )
123
+
124
+ @classmethod
125
+ def from_pb(cls, message: ChannelConfigPb) -> Self:
126
+ return cls(
127
+ name=message.name,
128
+ data_type=ChannelDataType.from_pb(message.data_type),
129
+ description=message.description,
130
+ unit=message.unit,
131
+ component=message.component,
132
+ bit_field_elements=[
133
+ ChannelBitFieldElement.from_pb(el) for el in message.bit_field_elements
134
+ ],
135
+ enum_types=[ChannelEnumType.from_pb(etype) for etype in message.enum_types],
136
+ )
137
+
138
+ def fqn(self) -> str:
139
+ """
140
+ The fully-qualified channel name of a channel called 'voltage' is simply `voltage`. The
141
+ fully qualified name of a channel called 'temperature' of component 'motor' is a `motor.temperature'.
142
+ """
143
+ return channel_fqn(self)
144
+
145
+
146
+ class ChannelBitFieldElement(AsProtobuf):
147
+ name: str
148
+ index: int
149
+ bit_count: int
150
+
151
+ def __init__(self, name: str, index: int, bit_count: int):
152
+ self.name = name
153
+ self.index = index
154
+ self.bit_count = bit_count
155
+
156
+ def as_pb(self, klass: Type[ChannelBitFieldElementPb]) -> ChannelBitFieldElementPb:
157
+ return klass(
158
+ name=self.name,
159
+ index=self.index,
160
+ bit_count=self.bit_count,
161
+ )
162
+
163
+ @classmethod
164
+ def from_pb(cls, message: ChannelBitFieldElementPb) -> Self:
165
+ return cls(
166
+ name=message.name,
167
+ index=message.index,
168
+ bit_count=message.bit_count,
169
+ )
170
+
171
+
172
+ class ChannelEnumType(AsProtobuf):
173
+ name: str
174
+ key: int
175
+
176
+ def __init__(self, name: str, key: int):
177
+ self.name = name
178
+ self.key = key
179
+
180
+ def as_pb(self, klass: Type[ChannelEnumTypePb]) -> ChannelEnumTypePb:
181
+ return klass(name=self.name, key=self.key)
182
+
183
+ @classmethod
184
+ def from_pb(cls, message: ChannelEnumTypePb) -> Self:
185
+ return cls(name=message.name, key=message.key)
186
+
187
+
188
+ class ChannelDataTypeStrRep(Enum):
189
+ DOUBLE = "double"
190
+ STRING = "string"
191
+ ENUM = "enum"
192
+ BIT_FIELD = "bit_field"
193
+ BOOL = "bool"
194
+ FLOAT = "float"
195
+ INT_32 = "int32"
196
+ INT_64 = "int64"
197
+ UINT_32 = "uint32"
198
+ UINT_64 = "uint64"
199
+
200
+ @staticmethod
201
+ def from_api_format(val: str) -> Optional["ChannelDataTypeStrRep"]:
202
+ try:
203
+ return {
204
+ "CHANNEL_DATA_TYPE_DOUBLE": ChannelDataTypeStrRep.DOUBLE,
205
+ "CHANNEL_DATA_TYPE_STRING": ChannelDataTypeStrRep.STRING,
206
+ "CHANNEL_DATA_TYPE_ENUM": ChannelDataTypeStrRep.ENUM,
207
+ "CHANNEL_DATA_TYPE_BIT_FIELD": ChannelDataTypeStrRep.BIT_FIELD,
208
+ "CHANNEL_DATA_TYPE_BOOL": ChannelDataTypeStrRep.BOOL,
209
+ "CHANNEL_DATA_TYPE_FLOAT": ChannelDataTypeStrRep.FLOAT,
210
+ "CHANNEL_DATA_TYPE_INT_32": ChannelDataTypeStrRep.INT_32,
211
+ "CHANNEL_DATA_TYPE_INT_64": ChannelDataTypeStrRep.INT_64,
212
+ "CHANNEL_DATA_TYPE_UINT_32": ChannelDataTypeStrRep.UINT_32,
213
+ "CHANNEL_DATA_TYPE_UINT_64": ChannelDataTypeStrRep.UINT_64,
214
+ }[val]
215
+ except KeyError:
216
+ return None
217
+
218
+
219
+ class ChannelDataType(Enum):
220
+ """
221
+ Utility enum class to simplify working with channel data-types generated from protobuf
222
+ """
223
+
224
+ DOUBLE = channel_pb.CHANNEL_DATA_TYPE_DOUBLE
225
+ STRING = channel_pb.CHANNEL_DATA_TYPE_STRING
226
+ ENUM = channel_pb.CHANNEL_DATA_TYPE_ENUM
227
+ BIT_FIELD = channel_pb.CHANNEL_DATA_TYPE_BIT_FIELD
228
+ BOOL = channel_pb.CHANNEL_DATA_TYPE_BOOL
229
+ FLOAT = channel_pb.CHANNEL_DATA_TYPE_FLOAT
230
+ INT_32 = channel_pb.CHANNEL_DATA_TYPE_INT_32
231
+ INT_64 = channel_pb.CHANNEL_DATA_TYPE_INT_64
232
+ UINT_32 = channel_pb.CHANNEL_DATA_TYPE_UINT_32
233
+ UINT_64 = channel_pb.CHANNEL_DATA_TYPE_UINT_64
234
+
235
+ @classmethod
236
+ def from_pb(cls, val: channel_pb.ChannelDataType.ValueType) -> "ChannelDataType":
237
+ if val == cls.DOUBLE.value:
238
+ return cls.DOUBLE
239
+ elif val == cls.STRING.value:
240
+ return cls.STRING
241
+ elif val == cls.ENUM.value:
242
+ return cls.ENUM
243
+ elif val == cls.BIT_FIELD.value:
244
+ return cls.BIT_FIELD
245
+ elif val == cls.BOOL.value:
246
+ return cls.BOOL
247
+ elif val == cls.FLOAT.value:
248
+ return cls.FLOAT
249
+ elif val == cls.INT_32.value:
250
+ return cls.INT_32
251
+ elif val == cls.INT_64.value:
252
+ return cls.INT_64
253
+ elif val == cls.UINT_32.value:
254
+ return cls.UINT_32
255
+ elif val == cls.UINT_64.value:
256
+ return cls.UINT_64
257
+ else:
258
+ raise ValueError(f"Unknown channel data type '{val}'.")
259
+
260
+ @classmethod
261
+ def from_str(cls, raw: str) -> Optional["ChannelDataType"]:
262
+ if raw.startswith("CHANNEL_DATA_TYPE_"):
263
+ val = ChannelDataTypeStrRep.from_api_format(raw)
264
+ if val is None:
265
+ return None
266
+ else:
267
+ try:
268
+ val = ChannelDataTypeStrRep(raw)
269
+ except ValueError:
270
+ return None
271
+
272
+ if val == ChannelDataTypeStrRep.DOUBLE:
273
+ return cls.DOUBLE
274
+ elif val == ChannelDataTypeStrRep.STRING:
275
+ return cls.STRING
276
+ elif val == ChannelDataTypeStrRep.ENUM:
277
+ return cls.ENUM
278
+ elif val == ChannelDataTypeStrRep.BIT_FIELD:
279
+ return cls.BIT_FIELD
280
+ elif val == ChannelDataTypeStrRep.BOOL:
281
+ return cls.BOOL
282
+ elif val == ChannelDataTypeStrRep.FLOAT:
283
+ return cls.FLOAT
284
+ elif val == ChannelDataTypeStrRep.INT_32:
285
+ return cls.INT_32
286
+ elif val == ChannelDataTypeStrRep.INT_64:
287
+ return cls.INT_64
288
+ elif val == ChannelDataTypeStrRep.UINT_32:
289
+ return cls.UINT_32
290
+ elif val == ChannelDataTypeStrRep.UINT_64:
291
+ return cls.UINT_64
292
+ else:
293
+ raise Exception("Unreachable")
294
+
295
+ def as_human_str(self, api_format: bool = False) -> str:
296
+ if self == ChannelDataType.DOUBLE:
297
+ return "CHANNEL_DATA_TYPE_DOUBLE" if api_format else ChannelDataTypeStrRep.DOUBLE.value
298
+ elif self == ChannelDataType.STRING:
299
+ return "CHANNEL_DATA_TYPE_STRING" if api_format else ChannelDataTypeStrRep.STRING.value
300
+ elif self == ChannelDataType.ENUM:
301
+ return "CHANNEL_DATA_TYPE_ENUM" if api_format else ChannelDataTypeStrRep.ENUM.value
302
+ elif self == ChannelDataType.BIT_FIELD:
303
+ return (
304
+ "CHANNEL_DATA_TYPE_BIT_FIELD"
305
+ if api_format
306
+ else ChannelDataTypeStrRep.BIT_FIELD.value
307
+ )
308
+ elif self == ChannelDataType.BOOL:
309
+ return "CHANNEL_DATA_TYPE_BOOL" if api_format else ChannelDataTypeStrRep.BOOL.value
310
+ elif self == ChannelDataType.FLOAT:
311
+ return "CHANNEL_DATA_TYPE_FLOAT" if api_format else ChannelDataTypeStrRep.FLOAT.value
312
+ elif self == ChannelDataType.INT_32:
313
+ return "CHANNEL_DATA_TYPE_INT_32" if api_format else ChannelDataTypeStrRep.INT_32.value
314
+ elif self == ChannelDataType.INT_64:
315
+ return "CHANNEL_DATA_TYPE_INT_64" if api_format else ChannelDataTypeStrRep.INT_64.value
316
+ elif self == ChannelDataType.UINT_32:
317
+ return (
318
+ "CHANNEL_DATA_TYPE_UINT_32" if api_format else ChannelDataTypeStrRep.UINT_32.value
319
+ )
320
+ elif self == ChannelDataType.UINT_64:
321
+ return (
322
+ "CHANNEL_DATA_TYPE_UINT_64" if api_format else ChannelDataTypeStrRep.UINT_64.value
323
+ )
324
+ else:
325
+ raise Exception("Unreachable.")
326
+
327
+
328
+ class _AbstractChannel(TypedDict):
329
+ channel_name: str
330
+ component: NotRequired[str]
331
+
332
+
333
+ def channel_fqn(
334
+ channel: Union[ChannelConfig, ChannelConfigPb, ChannelValue, ChannelPb, _AbstractChannel],
335
+ ) -> str:
336
+ """
337
+ Computes the fully qualified channel name.
338
+
339
+ The fully-qualified channel name of a channel called 'voltage' is simply `voltage'. The
340
+ fully qualified name of a channel called 'temperature' of component 'motor' is a `motor.temperature'.
341
+ """
342
+
343
+ if isinstance(channel, ChannelConfig):
344
+ return _channel_fqn(channel.name, channel.component)
345
+ elif isinstance(channel, ChannelConfigPb):
346
+ return _channel_fqn(channel.name, channel.component)
347
+ elif isinstance(channel, ChannelPb):
348
+ return _channel_fqn(channel.name, channel.component)
349
+ else:
350
+ component = channel.get("component", "")
351
+ channel_name = channel["channel_name"]
352
+ if len(component) == 0:
353
+ return channel_name
354
+ else:
355
+ return f"{component}.{channel_name}"
356
+
357
+
358
+ def string_value(val: str) -> IngestWithConfigDataChannelValue:
359
+ return IngestWithConfigDataChannelValue(string=val)
360
+
361
+
362
+ def double_value(val: float) -> IngestWithConfigDataChannelValue:
363
+ return IngestWithConfigDataChannelValue(double=val)
364
+
365
+
366
+ def float_value(val: float) -> IngestWithConfigDataChannelValue:
367
+ return IngestWithConfigDataChannelValue(float=val)
368
+
369
+
370
+ def bool_value(val: bool) -> IngestWithConfigDataChannelValue:
371
+ return IngestWithConfigDataChannelValue(bool=val)
372
+
373
+
374
+ def int32_value(val: int) -> IngestWithConfigDataChannelValue:
375
+ return IngestWithConfigDataChannelValue(int32=val)
376
+
377
+
378
+ def uint32_value(val: int) -> IngestWithConfigDataChannelValue:
379
+ return IngestWithConfigDataChannelValue(uint32=val)
380
+
381
+
382
+ def int64_value(val: int) -> IngestWithConfigDataChannelValue:
383
+ return IngestWithConfigDataChannelValue(int64=val)
384
+
385
+
386
+ def uint64_value(val: int) -> IngestWithConfigDataChannelValue:
387
+ return IngestWithConfigDataChannelValue(uint64=val)
388
+
389
+
390
+ def bit_field_value(val: bytes) -> IngestWithConfigDataChannelValue:
391
+ return IngestWithConfigDataChannelValue(bit_field=val)
392
+
393
+
394
+ def enum_value(val: int) -> IngestWithConfigDataChannelValue:
395
+ return IngestWithConfigDataChannelValue(enum=val)
396
+
397
+
398
+ def empty_value() -> IngestWithConfigDataChannelValue:
399
+ return IngestWithConfigDataChannelValue(empty=Empty())
400
+
401
+
402
+ def is_data_type(val: IngestWithConfigDataChannelValue, target_type: ChannelDataType) -> bool:
403
+ if target_type == ChannelDataType.DOUBLE:
404
+ return val.HasField("double")
405
+ elif target_type == ChannelDataType.STRING:
406
+ return val.HasField("string")
407
+ elif target_type == ChannelDataType.ENUM:
408
+ return val.HasField("enum")
409
+ elif target_type == ChannelDataType.BIT_FIELD:
410
+ return val.HasField("bit_field")
411
+ elif target_type == ChannelDataType.BOOL:
412
+ return val.HasField("bool")
413
+ elif target_type == ChannelDataType.FLOAT:
414
+ return val.HasField("float")
415
+ elif target_type == ChannelDataType.INT_32:
416
+ return val.HasField("int32")
417
+ elif target_type == ChannelDataType.INT_64:
418
+ return val.HasField("int64")
419
+ elif target_type == ChannelDataType.UINT_32:
420
+ return val.HasField("uint32")
421
+ elif target_type == ChannelDataType.UINT_64:
422
+ return val.HasField("uint64")
@@ -0,0 +1,3 @@
1
+ """
2
+ Contains the in memory representation of a telemetry config used to configure ingestion.
3
+ """