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
sift_py/__init__.py ADDED
@@ -0,0 +1,923 @@
1
+ """
2
+ `sift_py` is a Python module built on top of Sift's protocol buffers to ergonomically interface with
3
+ Sift's gRPC API, especially with regard to data ingestion and and rule evaluation. If there are any
4
+ words or concepts that you find yourself needing to familiarize yourself with, be sure to visit the
5
+ official [Sift documentation](https://docs.siftstack.com/glossary).
6
+
7
+ * [Introduction](#introduction)
8
+ - [Quickstart](#quickstart)
9
+ * [Telemetry Config](#telemetry-config)
10
+ - [Telemetry Config from YAML](#telemetry-config-from-yaml)
11
+ - [Telemetry Config YAML Schema](#telemetry-config-yaml-schema)
12
+ - [Named Expression Modules](#named-expression-modules)
13
+ * [Updating a Telemetry Config](#updating-a-telemetry-config)
14
+ - [Ingestion Client Key](#ingestion-client-key)
15
+ * [Ingestion Service](#ingestion-service)
16
+ - [Sending data to Sift](#sending-data-to-sift)
17
+ * [Ingestion Performance](#ingestion-performance)
18
+ - [Buffered Ingestion](#buffered-ingestion)
19
+ * [Downloading Telemetry](#downloading-telemetry)
20
+ * [File attachments](#file-attachments)
21
+ * [More Examples](#more-examples)
22
+
23
+ ## Introduction
24
+
25
+ The two fundamental components of this module are the following:
26
+ - `sift_py.ingestion.config.telemetry.TelemetryConfig` (telemetry config)
27
+ - `sift_py.ingestion.service.IngestionService` (ingestion service)
28
+
29
+ The telemetry config defines the schema of your telemetry. It is where you will declare your asset, channels and their components,
30
+ flows, and rules:
31
+ - `sift_py.ingestion.channel.ChannelConfig`
32
+ - `sift_py.ingestion.rule.config.RuleConfig`
33
+ - `sift_py.ingestion.flow.FlowConfig`
34
+
35
+ Once you have a telemetry config instantiated, you can then proceed to instantiate `sift_py.ingestion.service.IngestionService`
36
+ which is what's used to actually send Data to Sift.
37
+
38
+ ### Quickstart
39
+
40
+ The following example demonstrates how to create a simple telemetry config for an asset with a single channel
41
+ and a single rule, afterwhich we'll send a single data point to Sift for that channel.
42
+
43
+ ```python
44
+ from datetime import datetime, timezone
45
+
46
+ from sift_py.grpc.transport import SiftChannelConfig, use_sift_channel
47
+ from sift_py.ingestion.channel import (
48
+ ChannelBitFieldElement,
49
+ ChannelConfig,
50
+ ChannelDataType,
51
+ ChannelEnumType,
52
+ double_value
53
+ )
54
+ from sift_py.ingestion.service import IngestionService
55
+ from sift_py.ingestion.config.telemetry import FlowConfig, TelemetryConfig
56
+ from sift_py.ingestion.rule.config import (
57
+ RuleActionCreateDataReviewAnnotation,
58
+ RuleConfig,
59
+ )
60
+
61
+ # Create a channel config
62
+ temperature_channel = ChannelConfig(
63
+ name="temperature",
64
+ component="thruster",
65
+ data_type=ChannelDataType.DOUBLE,
66
+ description="temperature of thruster",
67
+ unit="Kelvin",
68
+ )
69
+
70
+ # Create a rule config referencing the above channel
71
+ overheating_rule = RuleConfig(
72
+ name="overheating",
73
+ description="Notify Ripley if thrusters get too hot",
74
+ expression='$1 > 400',
75
+ channel_references=[
76
+ {
77
+ "channel_reference": "$1",
78
+ "channel_config": temperature_channel,
79
+ },
80
+ ],
81
+ action=RuleActionCreateDataReviewAnnotation(
82
+ assignee="ellen.ripley@weylandcorp.com",
83
+ tags=["warning", "thruster"],
84
+ ),
85
+ ),
86
+
87
+ # Creating the telemetry config using the rules and channels
88
+ # described above
89
+ telemetry_config = TelemetryConfig(
90
+ asset_name="NostromoLV426",
91
+ ingestion_client_key="nostromo_lv_426",
92
+ rules=[overheating_rule],
93
+ flows=[
94
+ FlowConfig(
95
+ name="temperature_reading",
96
+ channels=[temperature_channel],
97
+ ),
98
+ ],
99
+ )
100
+
101
+
102
+ # Create a gRPC transport channel configured specifically for the Sift API
103
+ sift_channel_config = SiftChannelConfig(uri=SIFT_BASE_URI, apikey=SIFT_API_KEY)
104
+
105
+ with use_sift_channel(sift_channel_config) as channel:
106
+ # Create ingestion service using the telemetry config we just created
107
+ ingestion_service = IngestionService(
108
+ channel,
109
+ telemetry_config,
110
+ )
111
+
112
+ # Send data to Sift for the 'temperature_reading' flow
113
+ ingestion_service.try_ingest_flows({
114
+ "flow_name": "temperature_reading",
115
+ "timestamp": datetime.now(timezone.utc),
116
+ "channel_values": [
117
+ {
118
+ "channel_name": "temperature",
119
+ "component": "thruster",
120
+ "value": double_value(327)
121
+ },
122
+ ],
123
+ })
124
+ ```
125
+
126
+ ## Telemetry Config
127
+
128
+ There are currently two methods with which to initialize a telemetry config:
129
+ - `sift_py.ingestion.config.telemetry.TelemetryConfig.__init__`
130
+ - `sift_py.ingestion.config.telemetry.TelemetryConfig.try_from_yaml`
131
+
132
+ Both are equally valid and your choice to use one or the other largely depends on you and your team's preferred
133
+ workflow. The following sections will cover each initialization method.
134
+
135
+ ### Telemetry Config From Yaml
136
+
137
+ While the telemetry config can be declaratively initialized using using the telemetry config's initializer, `sift_py` also exposes an API
138
+ to initialize a telemetry config from a YAML file. The following is a simple demonstration.
139
+
140
+ Say that we had the following project structure:
141
+
142
+ ```
143
+ example
144
+ ├─ telemetry_configs
145
+ │ └─ nostromo_lv_426.yml
146
+ ├─ main.py
147
+ ├─ telemetry_config.py
148
+ └─ requirements.txt
149
+ ```
150
+
151
+ If our telemetry config is defined in the YAML file, `nostromo_lv_426.yml`, one of the ways in which
152
+ we might read that YAML file in as a `sift_py.ingestion.config.telemetry.TelemetryConfig` is to do the following:
153
+
154
+ ```python
155
+ from pathlib import Path
156
+
157
+ TELEMETRY_CONFIGS_DIR = Path().joinpath("telemetry_configs")
158
+
159
+ def nostromos_lv_426() -> TelemetryConfig:
160
+ telemetry_config_path = TELEMETRY_CONFIGS_DIR.joinpath("nostromo_lv_426.yml")
161
+ return TelemetryConfig.try_from_yaml(telemetry_config_path)
162
+ ```
163
+
164
+ As for the contents of the `nostromo_lv_426.yml`, file it might look something like this:
165
+
166
+ ```yaml
167
+ asset_name: NostromoLV426
168
+ ingestion_client_key: nostromo_lv_426
169
+
170
+ channels:
171
+ temperature_channel: &temperature_channel
172
+ name: temperature
173
+ component: thruster
174
+ data_type: double
175
+ description: temperature of the thruster
176
+ unit: Kelvin
177
+
178
+ rules:
179
+ - name: overheating
180
+ description: Notify Ripley if thrusters get too hot
181
+ expression: $1 > 400
182
+ channel_references:
183
+ - $1: *temperature_channel
184
+ type: review
185
+ assignee: ellen.ripley@weylandcorp.com
186
+ tags:
187
+ - warning
188
+ - thruster
189
+
190
+ flows:
191
+ - name: temperature_reading
192
+ channels:
193
+ - <<: *temperature_channel
194
+ ```
195
+
196
+ And with the telemetry config that we just created, we can then proceed to create an ingestion service
197
+ and begin data ingestion.
198
+
199
+ ### Telemetry Config YAML Schema
200
+
201
+ The following is the formal schema for a valid telemetry config in YAML. You can also see the `sift_py.ingestion.ingestion.config.yaml.spec` module
202
+ to see the schema in the for of Python classes.
203
+
204
+ ```yaml
205
+ schema:
206
+ description: |
207
+ A formal specification to create a telemetry config which is used
208
+ to stream data and evaluate rules using Sift's gRPC API.
209
+
210
+ asset_name:
211
+ type: string
212
+ description: The name of the asset to telemeter.
213
+
214
+ ingestion_client_key:
215
+ type: string
216
+ description: User-defined string-key that uniquely identifies this telemetry config.
217
+
218
+ organization_id:
219
+ type: string
220
+ description: Optional ID of user's organization. Required if user belongs to multiple organizations.
221
+
222
+ channels:
223
+ type: array
224
+ description: Sensors that send the data.
225
+ items:
226
+ type: object
227
+ properties:
228
+ name:
229
+ type: string
230
+ description: Name of the channel.
231
+ description:
232
+ type: string
233
+ description: Description of the channel.
234
+ unit:
235
+ type: string
236
+ description: Unit of measurement.
237
+ component:
238
+ type: string
239
+ description: Name of the component that the channel belongs to.
240
+ data_type:
241
+ type: string
242
+ enum: ["double", "string", "enum", "bit_field", "bool", "float", "int32", "int64", "uint32", "uint64"]
243
+ description: Type of the data associated with the channel.
244
+ enum_types:
245
+ type: array
246
+ items:
247
+ type: object
248
+ properties:
249
+ name:
250
+ type: string
251
+ description: Name of the enum type.
252
+ key:
253
+ type: integer
254
+ description: Key of the enum type.
255
+ description: Required if `data_type` is `enum`.
256
+ bit_field_elements:
257
+ type: array
258
+ description: Required if `data_type` is `bit_field`.
259
+ items:
260
+ type: object
261
+ properties:
262
+ name:
263
+ type: string
264
+ description: Name of the bit-field element.
265
+ index:
266
+ type: integer
267
+ description: Index of the bit-field element.
268
+ bit_count:
269
+ type: integer
270
+ description: Bit count of the bit-field element.
271
+
272
+ rules:
273
+ type: array
274
+ description: Rules that, when evaluated to a true, will perform some sort of action.
275
+ items:
276
+ type: object
277
+ properties:
278
+ name:
279
+ type: string
280
+ description: Name of the rule.
281
+ description:
282
+ type: string
283
+ description: Description of the rule.
284
+ expression:
285
+ oneOf:
286
+ - type: string
287
+ description: A string expression defining the rule logic.
288
+ - type: object
289
+ description: A reference to a named expression.
290
+ properties:
291
+ name:
292
+ type: string
293
+ description: Name of the named expression.
294
+ type:
295
+ type: string
296
+ enum: [phase, review]
297
+ description: Determines the action to perform if a rule gets evaluated to true.
298
+ assignee:
299
+ type: string
300
+ description: If 'type' is 'review', determines who to notify. Expects an email.
301
+ tags:
302
+ type: array
303
+ items:
304
+ type: string
305
+ description: Tags to associate with the rule.
306
+ channel_references:
307
+ type: array
308
+ description: A list of channel references that map to an actual channel. Use YAML anchors to reference channels.
309
+ items:
310
+ type: object
311
+ description: |
312
+ Key-value pair of string to channel. The channel should be a YAML anchor to a previously declared channel
313
+ in the top-level 'channels' property. The key should take the form of '$1', '$2', '$11', and do on. In YAML
314
+ it would look something like this:
315
+
316
+ ------------------------------------
317
+ channel_references:
318
+ - $1: *vehicle_state_channel
319
+ - $2: *voltage_channel
320
+ ------------------------------------
321
+ sub_expressions:
322
+ type: array
323
+ description: A list of sub-expressions which is a mapping of place-holders to sub-expressions.
324
+ items:
325
+ type: object
326
+ description: |
327
+ A sub-expression is made up of two components: A reference and the actual sub-expression. The sub-expression reference is
328
+ a string with a "$" prepended to another string comprised of characters in the following character set: `[a-zA-Z0-9_]`.
329
+ This reference should be mapped to the actual sub-expression. For example, say you have kinematic equations in `kinematics.yml`,
330
+ and the equation you're interested in using looks like the following:
331
+
332
+ ------------------------------------
333
+ kinetic_energy_gt:
334
+ 0.5 * $mass * $1 * $1 > $threshold
335
+ ------------------------------------
336
+
337
+ To properly use `kinetic_energy_gt` in your rule, it would look like the following:
338
+
339
+ ------------------------------------
340
+ rules:
341
+ - name: kinetic_energy
342
+ description: Tracks high energy output while in motion
343
+ type: review
344
+ assignee: bob@example.com
345
+ expression:
346
+ name: kinetic_energy_gt
347
+ channel_references:
348
+ - $1: *velocity_channel
349
+ sub_expressions:
350
+ - $mass: 10
351
+ - $threshold: 470
352
+ tags:
353
+ - nostromo
354
+ ------------------------------------
355
+ flows:
356
+ type: array
357
+ description: A list of named groups of channels that send data together.
358
+ items:
359
+ type: object
360
+ properties:
361
+ name:
362
+ type: string
363
+ description: Name of the flow.
364
+ channels:
365
+ type: array
366
+ items:
367
+ type: object
368
+ description: |
369
+ List of channels included in the flow. Should be a YAML anchor from a previously declared channel
370
+ in the top-level 'channels' property.
371
+ ```
372
+
373
+ ## Named Expression Modules
374
+
375
+ Often times you may find yourself needing to re-using more complex rule expressions across different telemetry
376
+ configs. If this is the case you might consider leveraging `named expressions` which allow you to reference the name
377
+ of an expression defined in another YAML file rather than defining it repeatedly across different telemetry configs.
378
+
379
+ As an example, say this is our current rule in our YAML telemetry config:
380
+
381
+ ```yaml
382
+ rules:
383
+ - name: kinetic_energy_gt
384
+ description: Tracks high energy output while in motion
385
+ type: review
386
+ assignee: cthulhu@rlyeh.com
387
+ expression: 0.5 * 10 * $1 * $1 > 470
388
+ channel_references:
389
+ - $1: *velocity_channel
390
+ ```
391
+
392
+ Instead of repeatedly writing that kinetic energy expression across different telemetry configs, you can move that expression
393
+ over to it's own named expression module YAML file which we'll call `kinematics.yml`, and then reference it by name in the
394
+ telemetry configs:
395
+
396
+ `kinematics.yml`
397
+ ```yaml
398
+ kinetic_energy_gt:
399
+ 0.5 * $mass * $1 * $1 > $threshold
400
+ rod_torque_gt:
401
+ (1 / 12) * $mass * $rod_length * $rod_length * $1
402
+ ```
403
+
404
+ `telemetry_config.py`
405
+ ```yaml
406
+ rules:
407
+ - name: kinetic_energy
408
+ description: Tracks high energy output while in motion
409
+ type: review
410
+ expression:
411
+ name: kinetic_energy_gt
412
+ channel_references:
413
+ - $1: *velocity_channel
414
+ sub_expressions:
415
+ - $mass: 10
416
+ - $threshold: 470
417
+ ```
418
+
419
+ In order for the telemetry configs to load in the named expression modules at run-time, all you need to do is provide the path
420
+ to the named expression module(s) wherever it may be. For example, given the following project structure:
421
+
422
+ ```
423
+ example
424
+ ├─ telemetry_configs
425
+ │ └─ nostromo_lv_426.yml
426
+ ├─ main.py
427
+ ├─ telemetry_config.py
428
+ └─ expression_modules
429
+ ├─ string.yml
430
+ └─ kinematics.yml
431
+ ```
432
+
433
+ Here is how we might load our telemetry config:
434
+
435
+ ```python
436
+ from pathlib import Path
437
+
438
+ from sift_py.ingestion.service import TelemetryConfig
439
+
440
+ TELEMETRY_CONFIGS_DIR = Path().joinpath("telemetry_configs")
441
+ EXPRESSION_MODULES_DIR = Path().joinpath("expression_modules")
442
+
443
+
444
+ def nostromos_lv_426() -> TelemetryConfig:
445
+ telemetry_config_path = TELEMETRY_CONFIGS_DIR.joinpath("nostromo_lv_426.yml")
446
+
447
+ return TelemetryConfig.try_from_yaml(
448
+ telemetry_config_path,
449
+ [
450
+ EXPRESSION_MODULES_DIR.joinpath("kinematics.yml"),
451
+ EXPRESSION_MODULES_DIR.joinpath("string.yml"),
452
+ ],
453
+ )
454
+ ```
455
+
456
+ ## Updating a Telemetry Config
457
+
458
+ The following section covers the situation in which you have an existing telemetry config that you would like to edit
459
+ for future telemetry and how to use the `ingestion_client_key`.
460
+
461
+ ### Ingestion Client Key
462
+
463
+ A `sift_py.ingestion.config.telemetry.TelemetryConfig` contains a field called `ingestion_client_key`
464
+ which is used by Sift to uniquely identify an existing telemetry config for an asset. For a given telemetry config
465
+ you are free to make the following changes and Sift will be able to pick it up without changing the `ingestion_client_key`:
466
+ - Adding new channels
467
+ - Removing existing channels (Need to also remove channel reference in the flow)
468
+ - Adding new flows
469
+ - Removing existing flows
470
+ - Adding new rules
471
+ - Updating existing rules
472
+
473
+ These can even be done on the fly at run-time.
474
+
475
+ The following changes, however, would require you to also update the `ingestion_client_key`, otherwise an exception will be raised
476
+ when a `sift_py.ingestion.service.IngestionService` is initialized.
477
+ - Updating an existing channel
478
+ - Adding a new channel to an existing flow
479
+
480
+ ## Ingestion Service
481
+
482
+ As mentioned previously, whereas a telemetry config defines the schema of your telemetry,
483
+ `sift_py.ingestion.service.IngestionService` is what's actually responsible for sending your data to Sift.
484
+
485
+ The two methods most folks will use to send data to Sift are the following:
486
+ - `sift_py.ingestion.service.IngestionService.try_ingest_flows`
487
+ - `sift_py.ingestion.service.IngestionService.ingest_flows`
488
+
489
+ Visit the function definitions to understand the differences between each.
490
+
491
+ Once you have generated a request using either of those methods
492
+ data is then sent to Sift using `sift_py.ingestion.service.IngestionService.ingest`.
493
+ The following are some examples illustrating generating data ingestion requests and sending them to Sift.
494
+
495
+ ### Sending Data to Sift
496
+
497
+ Suppose we have the following telemetry config with four configured instances of `sift_py.ingestion.flow.FlowConfig`.
498
+
499
+ ```python
500
+ def nostromos_lv_426() -> TelemetryConfig:
501
+ log_channel = ChannelConfig(
502
+ name="log",
503
+ data_type=ChannelDataType.STRING,
504
+ description="asset logs",
505
+ )
506
+ velocity_channel = ChannelConfig(
507
+ name="velocity",
508
+ data_type=ChannelDataType.DOUBLE,
509
+ description="speed",
510
+ unit="Miles Per Hour",
511
+ component="mainmotor",
512
+ )
513
+ voltage_channel = ChannelConfig(
514
+ name="voltage",
515
+ data_type=ChannelDataType.INT_32,
516
+ description="voltage at source",
517
+ unit="Volts",
518
+ )
519
+ vehicle_state_channel = ChannelConfig(
520
+ name="vehicle_state",
521
+ data_type=ChannelDataType.ENUM,
522
+ description="vehicle state",
523
+ enum_types=[
524
+ ChannelEnumType(name="Accelerating", key=0),
525
+ ChannelEnumType(name="Decelerating", key=1),
526
+ ChannelEnumType(name="Stopped", key=2),
527
+ ],
528
+ )
529
+ gpio_channel = ChannelConfig(
530
+ name="gpio",
531
+ data_type=ChannelDataType.BIT_FIELD,
532
+ description="on/off values for pins on gpio",
533
+ bit_field_elements=[
534
+ ChannelBitFieldElement(name="12v", index=0, bit_count=1),
535
+ ChannelBitFieldElement(name="charge", index=1, bit_count=2),
536
+ ChannelBitFieldElement(name="led", index=3, bit_count=4),
537
+ ChannelBitFieldElement(name="heater", index=7, bit_count=1),
538
+ ],
539
+ )
540
+
541
+ return TelemetryConfig(
542
+ asset_name="NostromoLV426",
543
+ ingestion_client_key="nostromo_lv_426",
544
+ flows=[
545
+ FlowConfig(
546
+ name="readings",
547
+ channels=[
548
+ velocity_channel,
549
+ voltage_channel,
550
+ vehicle_state_channel,
551
+ gpio_channel,
552
+ ],
553
+ ),
554
+ FlowConfig(
555
+ name="voltage",
556
+ channels=[voltage_channel],
557
+ ),
558
+ FlowConfig(
559
+ name="gpio_channel",
560
+ channels=[gpio_channel],
561
+ ),
562
+ FlowConfig(name="logs", channels=[log_channel]),
563
+ ],
564
+ )
565
+ ```
566
+
567
+ The following is an example of ingesting data for each flow using `sift_py.ingestion.service.IngestionService.try_ingest_flows`:
568
+
569
+ ```python
570
+ import time
571
+ from datetime import datetime, timezone
572
+
573
+ from sift_py.grpc.transport import SiftChannelConfig, use_sift_channel
574
+ from sift_py.ingestion.channel import (
575
+ ChannelBitFieldElement,
576
+ ChannelConfig,
577
+ ChannelDataType,
578
+ ChannelEnumType,
579
+ bit_field_value,
580
+ double_value,
581
+ enum_value,
582
+ int32_value,
583
+ string_value,
584
+ )
585
+ from sift_py.ingestion.service import IngestionService
586
+ from sift_py.ingestion.config.telemetry import FlowConfig, TelemetryConfig
587
+
588
+
589
+ telemetry_config = nostromos_lv_426()
590
+
591
+ sift_channel_config = SiftChannelConfig(uri=base_uri, apikey=apikey)
592
+
593
+ with use_sift_channel(sift_channel_config) as channel:
594
+ ingestion_service = IngestionService(
595
+ channel,
596
+ telemetry_config,
597
+ )
598
+
599
+ # Send data for the readings flow
600
+ ingestion_service.try_ingest_flows({
601
+ "flow_name": "readings",
602
+ "timestamp": datetime.now(timezone.utc),
603
+ "channel_values": [
604
+ {
605
+ "channel_name": "velocity",
606
+ "component": "mainmotor",
607
+ "value": double_value(10),
608
+ },
609
+ {
610
+ "channel_name": "voltage",
611
+ "value": int32_value(5),
612
+ },
613
+ {
614
+ "channel_name": "vehicle_state",
615
+ "value": enum_value(2),
616
+ },
617
+ {
618
+ "channel_name": "gpio",
619
+ "value": bit_field_value(bytes(int("00001001", 2)),
620
+ },
621
+ ],
622
+ })
623
+
624
+ # Send partial data for the readings flow
625
+ ingestion_service.try_ingest_flows({
626
+ "flow_name": "readings",
627
+ "timestamp": datetime.now(timezone.utc),
628
+ "channel_values": [
629
+ {
630
+ "channel_name": "velocity",
631
+ "component": "mainmotor",
632
+ "value": double_value(10),
633
+ },
634
+ {
635
+ "channel_name": "gpio",
636
+ "value": bit_field_value(bytes(int("00001001", 2)),
637
+ },
638
+ ],
639
+ })
640
+
641
+ # Send partial data for the logs flow
642
+ ingestion_service.try_ingest_flows({
643
+ "flow_name": "readings",
644
+ "timestamp": datetime.now(timezone.utc),
645
+ "channel_values": [
646
+ {
647
+ "channel_name": "logs",
648
+ "value": string_value("INFO: some message")
649
+ },
650
+ ],
651
+ })
652
+
653
+ # Send data for both logs and readings
654
+ ingestion_service.try_ingest_flows(
655
+ {
656
+ "flow_name": "readings",
657
+ "timestamp": datetime.now(timezone.utc),
658
+ "channel_values": [
659
+ {
660
+ "channel_name": "velocity",
661
+ "component": "mainmotor",
662
+ "value": double_value(10),
663
+ },
664
+ {
665
+ "channel_name": "voltage",
666
+ "value": int32_value(5),
667
+ },
668
+ {
669
+ "channel_name": "vehicle_state",
670
+ "value": enum_value(2),
671
+ },
672
+ {
673
+ "channel_name": "gpio",
674
+ "value": bit_field_value(bytes(int("00001001", 2)),
675
+ },
676
+ ],
677
+ },
678
+ {
679
+ "flow_name": "logs",
680
+ "timestamp": datetime.now(timezone.utc),
681
+ "channel_values": [
682
+ {
683
+ "channel_name": "logs",
684
+ "value": string_value("INFO: some message")
685
+ },
686
+ ],
687
+ },
688
+ )
689
+
690
+ ```
691
+
692
+ Alternatively, you may also use `sift_py.ingestion.service.IngestionService.ingest_flows`, but be sure
693
+ to read the documentation for that method to understand how to leverage it correctly. Unlike
694
+ `sift_py.ingestion.service.IngestionService.try_ingest_flows`, it will not perform any client-side validations.
695
+ This is useful when performance is critical. Do note, however, that the client-side validations done in `sift_py.ingestion.service.IngestionService.try_ingest_flows`
696
+ are pretty minimal and should not incur noticeable overhead.
697
+
698
+ ```python
699
+ import time
700
+ from datetime import datetime, timezone
701
+
702
+ from sift_py.grpc.transport import SiftChannelConfig, use_sift_channel
703
+ from sift_py.ingestion.channel import (
704
+ ChannelBitFieldElement,
705
+ ChannelConfig,
706
+ ChannelDataType,
707
+ ChannelEnumType,
708
+ bit_field_value,
709
+ empty_value,
710
+ double_value,
711
+ enum_value,
712
+ int32_value,
713
+ string_value,
714
+ )
715
+ from sift_py.ingestion.service import IngestionService
716
+ from sift_py.ingestion.config.telemetry import FlowConfig, TelemetryConfig
717
+
718
+
719
+ telemetry_config = nostromos_lv_426()
720
+
721
+ sift_channel_config = SiftChannelConfig(uri=base_uri, apikey=apikey)
722
+
723
+ with use_sift_channel(sift_channel_config) as channel:
724
+ ingestion_service = IngestionService(
725
+ channel,
726
+ telemetry_config,
727
+ )
728
+
729
+ # Send data for the readings flow
730
+ ingestion_service.ingest_flows({
731
+ "flow_name": "readings",
732
+ "timestamp": datetime.now(timezone.utc),
733
+ "channel_values": [
734
+ double_value(10),
735
+ int32_value(5),
736
+ enum_value(2),
737
+ bit_field_value(bytes(int("00001001", 2)),
738
+ ],
739
+ })
740
+
741
+ # Send partial data for the readings flow
742
+ ingestion_service.ingest_flows({
743
+ "flow_name": "readings",
744
+ "timestamp": datetime.now(timezone.utc),
745
+ "channel_values": [
746
+ double_value(10),
747
+ empty_value(),
748
+ empty_value(),
749
+ bit_field_value(bytes(int("00001001", 2)),
750
+ ],
751
+ })
752
+
753
+ # Send data for logs flow
754
+ ingestion_service.ingest_flows({
755
+ "flow_name": "logs",
756
+ "timestamp": datetime.now(timezone.utc),
757
+ "channel_values": [
758
+ string_value("INFO: some message"),
759
+ ],
760
+ })
761
+
762
+ # Send data for both logs and readings flow
763
+ ingestion_service.ingest_flows(
764
+ {
765
+ "flow_name": "readings",
766
+ "timestamp": datetime.now(timezone.utc),
767
+ "channel_values": [
768
+ double_value(10),
769
+ int32_value(5),
770
+ enum_value(2),
771
+ bit_field_value(bytes(int("00001001", 2)),
772
+ ],
773
+ },
774
+ {
775
+ "flow_name": "logs",
776
+ "timestamp": datetime.now(timezone.utc),
777
+ "channel_values": [
778
+ string_value("INFO: some message"),
779
+ ],
780
+ },
781
+ )
782
+
783
+ ```
784
+
785
+ ## Ingestion Performance
786
+
787
+ Depending on your ingestion setup there are some very common Python gotchas as it relates to gRPC that
788
+ hinders performance. The following are some examples of things you may want to avoid
789
+ when ingesting data into Sift:
790
+
791
+ 1. Avoid ingesting a high volume of data points in a hot loop. Prefer to ingest the data as a batch so that
792
+ serializing all outgoing requests can happen in one-fell swoop.
793
+
794
+ ```python
795
+ # Avoid this:
796
+ for flow in flows:
797
+ ingestion_service.try_ingest_flows(flow)
798
+
799
+ # Do this:
800
+ ingestion_service.try_ingest_flows(*flows)
801
+ ```
802
+
803
+ 2. Avoid sending too much data at once, otherwise you may encounter CPU-bound bottlenecks caused by
804
+ serializing a large amount of messages.
805
+
806
+ ```python
807
+ # Avoid this:
808
+ ingestion_service.try_ingest_flows(*a_very_large_amount_of_flows)
809
+ ```
810
+
811
+ To avoid having to deal with these pitfalls, prefer to leverage buffered ingestion.
812
+
813
+ ### Buffered Ingestion
814
+
815
+ `sift_py` offers an API to automatically buffer requests and send them in batches when the
816
+ buffer threshold is met. This ensures the following:
817
+ - You are not serializing, streaming, serializing, streaming, and so on, one record at a time.
818
+ - You are not spending too much time serializing a large amount of requests, and likewise,
819
+ spending too much time streaming a high volume of messages.
820
+
821
+ This API is available via the following:
822
+ - `sift_py.ingestion.service.IngestionService.buffered_ingestion`
823
+
824
+ The buffered ingestion mechanism simply handles the buffering logic and streams the data only
825
+ after the buffer threshold is met. The following is an example of how it might be used:
826
+
827
+ ```python
828
+ # Defaults to a buffer size of `sift_py.ingestion.buffer.DEFAULT_BUFFER_SIZE` requests.
829
+ with ingestion_service.buffered_ingestion() as buffered_ingestion:
830
+ buffered_ingestion.try_ingest_flows(*lots_of_flows)
831
+ buffered_ingestion.try_ingest_flows(*lots_more_flows)
832
+
833
+ # Custom buffer size of 750 requests
834
+ with ingestion_service.buffered_ingestion(750) as buffered_ingestion:
835
+ buffered_ingestion.try_ingest_flows(*lots_of_flows)
836
+ buffered_ingestion.try_ingest_flows(*lots_more_flows)
837
+ ```
838
+
839
+ Once the with-block ends, the remaining requests will be flushed from the buffer automatically,
840
+ but you may manually flush as well:
841
+
842
+ ```python
843
+ with ingestion_service.buffered_ingestion() as buffered_ingestion:
844
+ buffered_ingestion.try_ingest_flows(*lots_of_flows)
845
+ buffered_ingestion.flush()
846
+ ```
847
+
848
+ Visit the `sift_py.ingestion.service.IngestionService.buffered_ingestion` function definition
849
+ for further details.
850
+
851
+ ## Downloading Telemetry
852
+
853
+ To download your telemetry locally you'll want to make use of the `sift_py.data` module. Them module-level documentation
854
+ contains more details, but here is an example script demonstrating how to download data for multiple channels, putting them
855
+ into a `pandas` data frame, and writing the results out to a CSV:
856
+
857
+ ```python
858
+ import asyncio
859
+ import functools
860
+ import pandas as pd
861
+ from sift_py.data.query import ChannelQuery, DataQuery
862
+ from sift_py.grpc.transport import SiftChannelConfig, use_sift_async_channel
863
+ from sift_py.data.service import DataService
864
+
865
+
866
+ async def channel_demo():
867
+ channel_config: SiftChannelConfig = {
868
+ "apikey": "my-key"
869
+ "uri": "sift-uri"
870
+ }
871
+
872
+ async with use_sift_async_channel(channel_config) as channel:
873
+ data_service = DataService(channel)
874
+
875
+ query = DataQuery(
876
+ asset_name="NostromoLV426",
877
+ start_time="2024-07-04T18:09:08.555-07:00",
878
+ end_time="2024-07-04T18:09:11.556-07:00",
879
+ channels=[
880
+ ChannelQuery(
881
+ channel_name="voltage",
882
+ run_name="[NostromoLV426].1720141748.047512"
883
+ ),
884
+ ChannelQuery(
885
+ channel_name="velocity",
886
+ component="mainmotors",
887
+ run_name="[NostromoLV426].1720141748.047512",
888
+ ),
889
+ ChannelQuery(
890
+ channel_name="gpio",
891
+ run_name="[NostromoLV426].1720141748.047512",
892
+ ),
893
+ ],
894
+ )
895
+
896
+ result = await data_service.execute(query)
897
+
898
+ data_frames = [
899
+ pd.DataFrame(data.columns())
900
+ for data in result.channels("voltage", "mainmotors.velocity", "gpio.12v")
901
+ ]
902
+
903
+ merged_frame = functools.reduce(
904
+ lambda x, y: pd.merge_asof(x, y, on="time"), data_frames
905
+ )
906
+
907
+ merged_frame.to_csv("my_csv.csv")
908
+
909
+ if __name__ == "__main__":
910
+ asyncio.run(example())
911
+ ```
912
+
913
+ ## File attachments
914
+
915
+ See the module-level documentation for `sift_py.file_attachment` to learn uploading and downloading
916
+ file attachments to various entities such as runs, annotations, and annotation logs. Once file attachments
917
+ are created they become viewable in the Sift application.
918
+
919
+ ## More Examples
920
+
921
+ For more comphrensive examples demonstrating a little bit of everything, you may
922
+ visit the [examples directory](https://github.com/sift-stack/sift/tree/main/python/examples) in the project repo.
923
+ """