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.
- google/__init__.py +1 -0
- google/api/__init__.py +0 -0
- google/api/annotations_pb2.py +27 -0
- google/api/annotations_pb2.pyi +29 -0
- google/api/annotations_pb2_grpc.py +4 -0
- google/api/annotations_pb2_grpc.pyi +30 -0
- google/api/field_behavior_pb2.py +30 -0
- google/api/field_behavior_pb2.pyi +175 -0
- google/api/field_behavior_pb2_grpc.py +4 -0
- google/api/field_behavior_pb2_grpc.pyi +30 -0
- google/api/http_pb2.py +31 -0
- google/api/http_pb2.pyi +433 -0
- google/api/http_pb2_grpc.py +4 -0
- google/api/http_pb2_grpc.pyi +30 -0
- protoc_gen_openapiv2/__init__.py +0 -0
- protoc_gen_openapiv2/options/__init__.py +0 -0
- protoc_gen_openapiv2/options/annotations_pb2.py +27 -0
- protoc_gen_openapiv2/options/annotations_pb2.pyi +48 -0
- protoc_gen_openapiv2/options/annotations_pb2_grpc.py +4 -0
- protoc_gen_openapiv2/options/annotations_pb2_grpc.pyi +17 -0
- protoc_gen_openapiv2/options/openapiv2_pb2.py +132 -0
- protoc_gen_openapiv2/options/openapiv2_pb2.pyi +1533 -0
- protoc_gen_openapiv2/options/openapiv2_pb2_grpc.py +4 -0
- protoc_gen_openapiv2/options/openapiv2_pb2_grpc.pyi +17 -0
- sift/__init__.py +0 -0
- sift/annotation_logs/__init__.py +0 -0
- sift/annotation_logs/v1/__init__.py +0 -0
- sift/annotation_logs/v1/annotation_logs_pb2.py +115 -0
- sift/annotation_logs/v1/annotation_logs_pb2.pyi +370 -0
- sift/annotation_logs/v1/annotation_logs_pb2_grpc.py +135 -0
- sift/annotation_logs/v1/annotation_logs_pb2_grpc.pyi +84 -0
- sift/annotations/__init__.py +0 -0
- sift/annotations/v1/__init__.py +0 -0
- sift/annotations/v1/annotations_pb2.py +180 -0
- sift/annotations/v1/annotations_pb2.pyi +539 -0
- sift/annotations/v1/annotations_pb2_grpc.py +237 -0
- sift/annotations/v1/annotations_pb2_grpc.pyi +144 -0
- sift/assets/__init__.py +0 -0
- sift/assets/v1/__init__.py +0 -0
- sift/assets/v1/assets_pb2.py +90 -0
- sift/assets/v1/assets_pb2.pyi +235 -0
- sift/assets/v1/assets_pb2_grpc.py +168 -0
- sift/assets/v1/assets_pb2_grpc.pyi +101 -0
- sift/calculated_channels/__init__.py +0 -0
- sift/calculated_channels/v1/__init__.py +0 -0
- sift/calculated_channels/v1/calculated_channels_pb2.py +99 -0
- sift/calculated_channels/v1/calculated_channels_pb2.pyi +280 -0
- sift/calculated_channels/v1/calculated_channels_pb2_grpc.py +101 -0
- sift/calculated_channels/v1/calculated_channels_pb2_grpc.pyi +64 -0
- sift/campaigns/__init__.py +0 -0
- sift/campaigns/v1/__init__.py +0 -0
- sift/campaigns/v1/campaigns_pb2.py +144 -0
- sift/campaigns/v1/campaigns_pb2.pyi +383 -0
- sift/campaigns/v1/campaigns_pb2_grpc.py +169 -0
- sift/campaigns/v1/campaigns_pb2_grpc.pyi +104 -0
- sift/channel_schemas/__init__.py +0 -0
- sift/channel_schemas/v1/__init__.py +0 -0
- sift/channel_schemas/v1/channel_schemas_pb2.py +69 -0
- sift/channel_schemas/v1/channel_schemas_pb2.pyi +117 -0
- sift/channel_schemas/v1/channel_schemas_pb2_grpc.py +101 -0
- sift/channel_schemas/v1/channel_schemas_pb2_grpc.pyi +64 -0
- sift/channels/__init__.py +0 -0
- sift/channels/v2/__init__.py +0 -0
- sift/channels/v2/channels_pb2.py +88 -0
- sift/channels/v2/channels_pb2.pyi +183 -0
- sift/channels/v2/channels_pb2_grpc.py +101 -0
- sift/channels/v2/channels_pb2_grpc.pyi +64 -0
- sift/common/__init__.py +0 -0
- sift/common/type/__init__.py +0 -0
- sift/common/type/v1/__init__.py +0 -0
- sift/common/type/v1/channel_bit_field_element_pb2.py +34 -0
- sift/common/type/v1/channel_bit_field_element_pb2.pyi +33 -0
- sift/common/type/v1/channel_bit_field_element_pb2_grpc.py +4 -0
- sift/common/type/v1/channel_bit_field_element_pb2_grpc.pyi +17 -0
- sift/common/type/v1/channel_data_type_pb2.py +29 -0
- sift/common/type/v1/channel_data_type_pb2.pyi +50 -0
- sift/common/type/v1/channel_data_type_pb2_grpc.py +4 -0
- sift/common/type/v1/channel_data_type_pb2_grpc.pyi +17 -0
- sift/common/type/v1/channel_enum_type_pb2.py +32 -0
- sift/common/type/v1/channel_enum_type_pb2.pyi +29 -0
- sift/common/type/v1/channel_enum_type_pb2_grpc.py +4 -0
- sift/common/type/v1/channel_enum_type_pb2_grpc.pyi +17 -0
- sift/common/type/v1/organization_pb2.py +27 -0
- sift/common/type/v1/organization_pb2.pyi +29 -0
- sift/common/type/v1/organization_pb2_grpc.py +4 -0
- sift/common/type/v1/organization_pb2_grpc.pyi +17 -0
- sift/common/type/v1/resource_identifier_pb2.py +46 -0
- sift/common/type/v1/resource_identifier_pb2.pyi +145 -0
- sift/common/type/v1/resource_identifier_pb2_grpc.py +4 -0
- sift/common/type/v1/resource_identifier_pb2_grpc.pyi +17 -0
- sift/common/type/v1/user_pb2.py +33 -0
- sift/common/type/v1/user_pb2.pyi +36 -0
- sift/common/type/v1/user_pb2_grpc.py +4 -0
- sift/common/type/v1/user_pb2_grpc.pyi +17 -0
- sift/data/__init__.py +0 -0
- sift/data/v1/__init__.py +0 -0
- sift/data/v1/data_pb2.py +212 -0
- sift/data/v1/data_pb2.pyi +745 -0
- sift/data/v1/data_pb2_grpc.py +67 -0
- sift/data/v1/data_pb2_grpc.pyi +44 -0
- sift/ingest/__init__.py +0 -0
- sift/ingest/v1/__init__.py +0 -0
- sift/ingest/v1/ingest_pb2.py +35 -0
- sift/ingest/v1/ingest_pb2.pyi +118 -0
- sift/ingest/v1/ingest_pb2_grpc.py +66 -0
- sift/ingest/v1/ingest_pb2_grpc.pyi +41 -0
- sift/ingestion_configs/__init__.py +0 -0
- sift/ingestion_configs/v1/__init__.py +0 -0
- sift/ingestion_configs/v1/ingestion_configs_pb2.py +115 -0
- sift/ingestion_configs/v1/ingestion_configs_pb2.pyi +332 -0
- sift/ingestion_configs/v1/ingestion_configs_pb2_grpc.py +203 -0
- sift/ingestion_configs/v1/ingestion_configs_pb2_grpc.pyi +124 -0
- sift/notifications/__init__.py +0 -0
- sift/notifications/v1/__init__.py +0 -0
- sift/notifications/v1/notifications_pb2.py +64 -0
- sift/notifications/v1/notifications_pb2.pyi +225 -0
- sift/notifications/v1/notifications_pb2_grpc.py +101 -0
- sift/notifications/v1/notifications_pb2_grpc.pyi +64 -0
- sift/ping/__init__.py +0 -0
- sift/ping/v1/__init__.py +0 -0
- sift/ping/v1/ping_pb2.py +38 -0
- sift/ping/v1/ping_pb2.pyi +36 -0
- sift/ping/v1/ping_pb2_grpc.py +66 -0
- sift/ping/v1/ping_pb2_grpc.pyi +41 -0
- sift/remote_files/__init__.py +0 -0
- sift/remote_files/v1/__init__.py +0 -0
- sift/remote_files/v1/remote_files_pb2.py +174 -0
- sift/remote_files/v1/remote_files_pb2.pyi +472 -0
- sift/remote_files/v1/remote_files_pb2_grpc.py +271 -0
- sift/remote_files/v1/remote_files_pb2_grpc.pyi +164 -0
- sift/report_templates/__init__.py +0 -0
- sift/report_templates/v1/__init__.py +0 -0
- sift/report_templates/v1/report_templates_pb2.py +146 -0
- sift/report_templates/v1/report_templates_pb2.pyi +381 -0
- sift/report_templates/v1/report_templates_pb2_grpc.py +169 -0
- sift/report_templates/v1/report_templates_pb2_grpc.pyi +104 -0
- sift/reports/__init__.py +0 -0
- sift/reports/v1/__init__.py +0 -0
- sift/reports/v1/reports_pb2.py +193 -0
- sift/reports/v1/reports_pb2.pyi +562 -0
- sift/reports/v1/reports_pb2_grpc.py +205 -0
- sift/reports/v1/reports_pb2_grpc.pyi +136 -0
- sift/rule_evaluation/__init__.py +0 -0
- sift/rule_evaluation/v1/__init__.py +0 -0
- sift/rule_evaluation/v1/rule_evaluation_pb2.py +89 -0
- sift/rule_evaluation/v1/rule_evaluation_pb2.pyi +263 -0
- sift/rule_evaluation/v1/rule_evaluation_pb2_grpc.py +101 -0
- sift/rule_evaluation/v1/rule_evaluation_pb2_grpc.pyi +64 -0
- sift/rules/__init__.py +0 -0
- sift/rules/v1/__init__.py +0 -0
- sift/rules/v1/rules_pb2.py +420 -0
- sift/rules/v1/rules_pb2.pyi +1355 -0
- sift/rules/v1/rules_pb2_grpc.py +577 -0
- sift/rules/v1/rules_pb2_grpc.pyi +351 -0
- sift/runs/__init__.py +0 -0
- sift/runs/v2/__init__.py +0 -0
- sift/runs/v2/runs_pb2.py +150 -0
- sift/runs/v2/runs_pb2.pyi +413 -0
- sift/runs/v2/runs_pb2_grpc.py +271 -0
- sift/runs/v2/runs_pb2_grpc.pyi +164 -0
- sift/saved_searches/__init__.py +0 -0
- sift/saved_searches/v1/__init__.py +0 -0
- sift/saved_searches/v1/saved_searches_pb2.py +144 -0
- sift/saved_searches/v1/saved_searches_pb2.pyi +385 -0
- sift/saved_searches/v1/saved_searches_pb2_grpc.py +237 -0
- sift/saved_searches/v1/saved_searches_pb2_grpc.pyi +144 -0
- sift/tags/__init__.py +0 -0
- sift/tags/v1/__init__.py +0 -0
- sift/tags/v1/tags_pb2.py +49 -0
- sift/tags/v1/tags_pb2.pyi +71 -0
- sift/tags/v1/tags_pb2_grpc.py +4 -0
- sift/tags/v1/tags_pb2_grpc.pyi +17 -0
- sift/users/__init__.py +0 -0
- sift/users/v2/__init__.py +0 -0
- sift/users/v2/users_pb2.py +61 -0
- sift/users/v2/users_pb2.pyi +142 -0
- sift/users/v2/users_pb2_grpc.py +135 -0
- sift/users/v2/users_pb2_grpc.pyi +84 -0
- sift/views/__init__.py +0 -0
- sift/views/v1/__init__.py +0 -0
- sift/views/v1/views_pb2.py +130 -0
- sift/views/v1/views_pb2.pyi +466 -0
- sift/views/v1/views_pb2_grpc.py +305 -0
- sift/views/v1/views_pb2_grpc.pyi +184 -0
- sift_grafana/py.typed +0 -0
- sift_grafana/sift_query_model.py +64 -0
- sift_py/__init__.py +923 -0
- sift_py/_internal/__init__.py +5 -0
- sift_py/_internal/cel.py +18 -0
- sift_py/_internal/channel.py +42 -0
- sift_py/_internal/convert/__init__.py +3 -0
- sift_py/_internal/convert/json.py +24 -0
- sift_py/_internal/convert/protobuf.py +34 -0
- sift_py/_internal/convert/timestamp.py +9 -0
- sift_py/_internal/test_util/__init__.py +0 -0
- sift_py/_internal/test_util/channel.py +136 -0
- sift_py/_internal/test_util/fn.py +14 -0
- sift_py/_internal/test_util/server_interceptor.py +62 -0
- sift_py/_internal/time.py +48 -0
- sift_py/_internal/user.py +39 -0
- sift_py/data/__init__.py +171 -0
- sift_py/data/_channel.py +38 -0
- sift_py/data/_deserialize.py +208 -0
- sift_py/data/_deserialize_test.py +134 -0
- sift_py/data/_service_test.py +276 -0
- sift_py/data/_validate.py +10 -0
- sift_py/data/error.py +5 -0
- sift_py/data/query.py +299 -0
- sift_py/data/service.py +497 -0
- sift_py/data_import/__init__.py +130 -0
- sift_py/data_import/_config.py +167 -0
- sift_py/data_import/_config_test.py +166 -0
- sift_py/data_import/_csv_test.py +395 -0
- sift_py/data_import/_status_test.py +176 -0
- sift_py/data_import/_tdms_test.py +238 -0
- sift_py/data_import/ch10.py +157 -0
- sift_py/data_import/config.py +19 -0
- sift_py/data_import/csv.py +259 -0
- sift_py/data_import/status.py +113 -0
- sift_py/data_import/tdms.py +206 -0
- sift_py/data_import/tempfile.py +30 -0
- sift_py/data_import/time_format.py +39 -0
- sift_py/error.py +11 -0
- sift_py/file_attachment/__init__.py +88 -0
- sift_py/file_attachment/_internal/__init__.py +0 -0
- sift_py/file_attachment/_internal/download.py +13 -0
- sift_py/file_attachment/_internal/upload.py +100 -0
- sift_py/file_attachment/_service_test.py +161 -0
- sift_py/file_attachment/entity.py +30 -0
- sift_py/file_attachment/metadata.py +107 -0
- sift_py/file_attachment/service.py +142 -0
- sift_py/grpc/__init__.py +15 -0
- sift_py/grpc/_async_interceptors/__init__.py +0 -0
- sift_py/grpc/_async_interceptors/base.py +72 -0
- sift_py/grpc/_async_interceptors/metadata.py +36 -0
- sift_py/grpc/_interceptors/__init__.py +0 -0
- sift_py/grpc/_interceptors/base.py +61 -0
- sift_py/grpc/_interceptors/context.py +25 -0
- sift_py/grpc/_interceptors/metadata.py +33 -0
- sift_py/grpc/_retry.py +70 -0
- sift_py/grpc/keepalive.py +34 -0
- sift_py/grpc/transport.py +250 -0
- sift_py/grpc/transport_test.py +170 -0
- sift_py/ingestion/__init__.py +6 -0
- sift_py/ingestion/_internal/__init__.py +6 -0
- sift_py/ingestion/_internal/channel.py +12 -0
- sift_py/ingestion/_internal/error.py +10 -0
- sift_py/ingestion/_internal/ingest.py +350 -0
- sift_py/ingestion/_internal/ingest_test.py +357 -0
- sift_py/ingestion/_internal/ingestion_config.py +130 -0
- sift_py/ingestion/_internal/run.py +46 -0
- sift_py/ingestion/_service_test.py +478 -0
- sift_py/ingestion/buffer.py +189 -0
- sift_py/ingestion/channel.py +422 -0
- sift_py/ingestion/config/__init__.py +3 -0
- sift_py/ingestion/config/telemetry.py +281 -0
- sift_py/ingestion/config/telemetry_test.py +405 -0
- sift_py/ingestion/config/yaml/__init__.py +0 -0
- sift_py/ingestion/config/yaml/error.py +44 -0
- sift_py/ingestion/config/yaml/load.py +126 -0
- sift_py/ingestion/config/yaml/spec.py +58 -0
- sift_py/ingestion/config/yaml/test_load.py +25 -0
- sift_py/ingestion/flow.py +73 -0
- sift_py/ingestion/manager.py +99 -0
- sift_py/ingestion/rule/__init__.py +4 -0
- sift_py/ingestion/rule/config.py +11 -0
- sift_py/ingestion/service.py +237 -0
- sift_py/py.typed +0 -0
- sift_py/report_templates/__init__.py +0 -0
- sift_py/report_templates/_config_test.py +34 -0
- sift_py/report_templates/_service_test.py +94 -0
- sift_py/report_templates/config.py +36 -0
- sift_py/report_templates/service.py +171 -0
- sift_py/rest.py +29 -0
- sift_py/rule/__init__.py +0 -0
- sift_py/rule/_config_test.py +109 -0
- sift_py/rule/_service_test.py +168 -0
- sift_py/rule/config.py +229 -0
- sift_py/rule/service.py +484 -0
- sift_py/yaml/__init__.py +0 -0
- sift_py/yaml/_channel_test.py +169 -0
- sift_py/yaml/_rule_test.py +207 -0
- sift_py/yaml/channel.py +224 -0
- sift_py/yaml/report_templates.py +73 -0
- sift_py/yaml/rule.py +321 -0
- sift_py/yaml/utils.py +15 -0
- sift_stack_py-0.3.2.dist-info/LICENSE +7 -0
- sift_stack_py-0.3.2.dist-info/METADATA +109 -0
- sift_stack_py-0.3.2.dist-info/RECORD +291 -0
- sift_stack_py-0.3.2.dist-info/WHEEL +5 -0
- 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
|
+
"""
|