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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Any, List, Optional
|
|
2
|
+
|
|
3
|
+
from typing_extensions import Self
|
|
4
|
+
|
|
5
|
+
_PREFIX = " "
|
|
6
|
+
_YAML_SEP = "---"
|
|
7
|
+
_LIST_ITEM_PREFIX = "- "
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class YamlConfigError(Exception):
|
|
11
|
+
"""
|
|
12
|
+
When the YAML config has missing or invalid properties.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
message: str
|
|
16
|
+
|
|
17
|
+
def __init__(self, message: str):
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def _invalid_property(
|
|
22
|
+
cls,
|
|
23
|
+
actual_value: Any,
|
|
24
|
+
key_name: str,
|
|
25
|
+
expected_type: str,
|
|
26
|
+
ancestors: Optional[List[str]] = None,
|
|
27
|
+
) -> Self:
|
|
28
|
+
header = f"Expected '{key_name.lstrip(_LIST_ITEM_PREFIX)}' to be <{expected_type}> but it is <{type(actual_value).__name__}>\n{_YAML_SEP}\n"
|
|
29
|
+
|
|
30
|
+
if ancestors is None or len(ancestors) == 0:
|
|
31
|
+
return cls(f"{header}{key_name}: <{expected_type}>")
|
|
32
|
+
|
|
33
|
+
key_value_path = ""
|
|
34
|
+
|
|
35
|
+
current_indentation_level = 0
|
|
36
|
+
|
|
37
|
+
for ancestor in ancestors:
|
|
38
|
+
key_value_path += f"{_PREFIX * current_indentation_level}{ancestor}:\n"
|
|
39
|
+
current_indentation_level += 1
|
|
40
|
+
|
|
41
|
+
key_value_path += f"{_PREFIX * current_indentation_level}{key_name}: <{expected_type}>"
|
|
42
|
+
output = f"{header}{key_value_path}"
|
|
43
|
+
|
|
44
|
+
return cls(output)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Dict, List, cast
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
import sift_py.yaml.rule as rule_yaml
|
|
7
|
+
from sift_py.ingestion.config.yaml.error import YamlConfigError
|
|
8
|
+
from sift_py.ingestion.config.yaml.spec import (
|
|
9
|
+
FlowYamlSpec,
|
|
10
|
+
TelemetryConfigYamlSpec,
|
|
11
|
+
)
|
|
12
|
+
from sift_py.yaml.channel import ChannelConfigYamlSpec, _validate_channel, _validate_channel_anchor
|
|
13
|
+
from sift_py.yaml.rule import RuleYamlSpec
|
|
14
|
+
from sift_py.yaml.utils import _type_fqn
|
|
15
|
+
|
|
16
|
+
load_named_expression_modules = rule_yaml.load_named_expression_modules
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_and_validate(path: Path) -> TelemetryConfigYamlSpec:
|
|
20
|
+
"""
|
|
21
|
+
Reads in the telemetry config YAML file found at `path` and validates it. Any errors that may occur at the parsing
|
|
22
|
+
step will return an error whose source is the `yaml` package. Any errors that may occur during the
|
|
23
|
+
validation step will return a `sift_py.ingestion.config.yaml.error.YamlConfigError`.
|
|
24
|
+
"""
|
|
25
|
+
raw_config = _read_yaml(path)
|
|
26
|
+
return _validate_yaml(raw_config)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _validate_yaml(raw_config: Dict[Any, Any]) -> TelemetryConfigYamlSpec:
|
|
30
|
+
asset_name = raw_config.get("asset_name")
|
|
31
|
+
|
|
32
|
+
if not isinstance(asset_name, str):
|
|
33
|
+
raise YamlConfigError._invalid_property(asset_name, "asset_name", "str")
|
|
34
|
+
|
|
35
|
+
ingestion_client_key = raw_config.get("ingestion_client_key")
|
|
36
|
+
|
|
37
|
+
if not isinstance(ingestion_client_key, str):
|
|
38
|
+
raise YamlConfigError._invalid_property(ingestion_client_key, "ingestion_client_key", "str")
|
|
39
|
+
|
|
40
|
+
organization_id = raw_config.get("organization_id")
|
|
41
|
+
|
|
42
|
+
if organization_id is not None and not isinstance(organization_id, str):
|
|
43
|
+
raise YamlConfigError._invalid_property(ingestion_client_key, "organization_id", "str")
|
|
44
|
+
|
|
45
|
+
channels = raw_config.get("channels")
|
|
46
|
+
|
|
47
|
+
if channels is not None:
|
|
48
|
+
if not isinstance(channels, dict):
|
|
49
|
+
raise YamlConfigError._invalid_property(
|
|
50
|
+
channels,
|
|
51
|
+
"channels",
|
|
52
|
+
f"Dict[str, {ChannelConfigYamlSpec}]",
|
|
53
|
+
None,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
for anchor, channel_config in cast(Dict[Any, Any], channels).items():
|
|
57
|
+
_validate_channel_anchor(anchor)
|
|
58
|
+
_validate_channel(channel_config)
|
|
59
|
+
|
|
60
|
+
rules = raw_config.get("rules")
|
|
61
|
+
|
|
62
|
+
if rules is not None:
|
|
63
|
+
if not isinstance(rules, list):
|
|
64
|
+
raise YamlConfigError._invalid_property(
|
|
65
|
+
rules,
|
|
66
|
+
"rules",
|
|
67
|
+
f"List[{_type_fqn(RuleYamlSpec)}]",
|
|
68
|
+
None,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
for rule in cast(List[Any], rules):
|
|
72
|
+
rule_yaml._validate_rule(rule)
|
|
73
|
+
|
|
74
|
+
flows = raw_config.get("flows")
|
|
75
|
+
|
|
76
|
+
if flows is not None:
|
|
77
|
+
if not isinstance(flows, list):
|
|
78
|
+
raise YamlConfigError._invalid_property(
|
|
79
|
+
flows,
|
|
80
|
+
"flows",
|
|
81
|
+
f"List[{_type_fqn(FlowYamlSpec)}]",
|
|
82
|
+
None,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
for flow in cast(List[Any], flows):
|
|
86
|
+
_validate_flow(flow)
|
|
87
|
+
|
|
88
|
+
return cast(TelemetryConfigYamlSpec, raw_config)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _read_yaml(path: Path) -> Dict[Any, Any]:
|
|
92
|
+
with open(path, "r") as f:
|
|
93
|
+
return cast(Dict[Any, Any], yaml.safe_load(f.read()))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _validate_flow(val: Any):
|
|
97
|
+
flow = cast(Dict[Any, Any], val)
|
|
98
|
+
|
|
99
|
+
name = flow.get("name")
|
|
100
|
+
|
|
101
|
+
if not isinstance(name, str):
|
|
102
|
+
raise YamlConfigError._invalid_property(
|
|
103
|
+
name,
|
|
104
|
+
"- name",
|
|
105
|
+
"str",
|
|
106
|
+
["flows"],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
channels = flow.get("channels")
|
|
110
|
+
|
|
111
|
+
if channels is not None:
|
|
112
|
+
if not isinstance(channels, list):
|
|
113
|
+
raise YamlConfigError._invalid_property(
|
|
114
|
+
channels,
|
|
115
|
+
"channels",
|
|
116
|
+
f"List<{ChannelConfigYamlSpec}>",
|
|
117
|
+
["flows"],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
for channel in cast(List[Any], channels):
|
|
121
|
+
try:
|
|
122
|
+
_validate_channel(channel)
|
|
123
|
+
except YamlConfigError as err:
|
|
124
|
+
raise YamlConfigError(
|
|
125
|
+
f"Flow '{name}' contains an invalid channel reference:\n{err}"
|
|
126
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Formal specification of the types that `sift_py` expects when loading a telemetry config from a YAML file.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Dict, List
|
|
8
|
+
|
|
9
|
+
from typing_extensions import NotRequired, TypedDict
|
|
10
|
+
|
|
11
|
+
import sift_py.yaml.channel as channel_yaml
|
|
12
|
+
import sift_py.yaml.rule as rule_yaml
|
|
13
|
+
|
|
14
|
+
RuleYamlSpec = rule_yaml.RuleYamlSpec
|
|
15
|
+
NamedExpressionYamlSpec = rule_yaml.NamedExpressionYamlSpec
|
|
16
|
+
ChannelConfigYamlSpec = channel_yaml.ChannelConfigYamlSpec
|
|
17
|
+
ChannelEnumTypeYamlSpec = channel_yaml.ChannelEnumTypeYamlSpec
|
|
18
|
+
ChannelBitFieldElementYamlSpec = channel_yaml.ChannelBitFieldElementYamlSpec
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TelemetryConfigYamlSpec(TypedDict):
|
|
22
|
+
"""
|
|
23
|
+
Formal spec that defines what the telemetry config should look like in YAML.
|
|
24
|
+
|
|
25
|
+
`asset_name`: The name of the asset to telemeter.
|
|
26
|
+
`ingestion_client_key`: User-defined string-key that uniquely identifies this telemetry config.
|
|
27
|
+
`organization_id`: Optional ID of user's organization. Required if user belongs to multiple orgs.
|
|
28
|
+
`channels`: Sensors that send the data.
|
|
29
|
+
`rules`: Rules that, when evaluated to a true, will perform some sort of acction.
|
|
30
|
+
`flows`: A list of named groups of channels that send data together.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
asset_name: str
|
|
34
|
+
ingestion_client_key: str
|
|
35
|
+
organization_id: NotRequired[str]
|
|
36
|
+
channels: Dict[str, ChannelConfigYamlSpec]
|
|
37
|
+
rules: NotRequired[List[RuleYamlSpec]]
|
|
38
|
+
flows: NotRequired[List[FlowYamlSpec]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FlowYamlSpec(TypedDict):
|
|
42
|
+
"""
|
|
43
|
+
Formal spec that defines what a flow should look like in YAML.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
channels: List[ChannelConfigYamlSpec]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class YamlConfigError(Exception):
|
|
51
|
+
"""
|
|
52
|
+
When the YAML config has missing or invalid properties.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
message: str
|
|
56
|
+
|
|
57
|
+
def __init__(self, message: str):
|
|
58
|
+
super().__init__(message)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from sift_py.ingestion.config.yaml import load
|
|
4
|
+
from sift_py.ingestion.config.yaml.error import YamlConfigError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test__validate_flow():
|
|
8
|
+
load._validate_flow(
|
|
9
|
+
{
|
|
10
|
+
"name": "reading",
|
|
11
|
+
"channels": [
|
|
12
|
+
{"name": "voltage", "data_type": "double"},
|
|
13
|
+
],
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
with pytest.raises(YamlConfigError):
|
|
18
|
+
load._validate_flow(
|
|
19
|
+
{
|
|
20
|
+
"name": "reading",
|
|
21
|
+
"channels": [
|
|
22
|
+
{"name": "voltage", "data_type": "dubble"},
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Dict, List, Type, TypedDict
|
|
5
|
+
|
|
6
|
+
from sift.ingest.v1.ingest_pb2 import IngestWithConfigDataChannelValue
|
|
7
|
+
from sift.ingestion_configs.v1.ingestion_configs_pb2 import (
|
|
8
|
+
ChannelConfig as ChannelConfigPb,
|
|
9
|
+
)
|
|
10
|
+
from sift.ingestion_configs.v1.ingestion_configs_pb2 import (
|
|
11
|
+
FlowConfig as FlowConfigPb,
|
|
12
|
+
)
|
|
13
|
+
from typing_extensions import Self
|
|
14
|
+
|
|
15
|
+
from sift_py._internal.convert.protobuf import AsProtobuf
|
|
16
|
+
from sift_py.ingestion.channel import ChannelConfig, ChannelValue, channel_fqn
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FlowConfig(AsProtobuf):
|
|
20
|
+
"""
|
|
21
|
+
Describes a flow which is a set of channels whose values are often ingested together, allowing
|
|
22
|
+
users to send multiple data points for multiple channels in a single request.
|
|
23
|
+
|
|
24
|
+
`channel_by_fqn`:
|
|
25
|
+
A mapping of a channel's fully-qualified name to the index of the `sift_py.ingestion.channel.ChannelConfig`
|
|
26
|
+
as it appears in the `channels` attribute.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
channels: List[ChannelConfig]
|
|
31
|
+
channel_by_fqn: Dict[str, int]
|
|
32
|
+
|
|
33
|
+
def __init__(self, name: str, channels: List[ChannelConfig]):
|
|
34
|
+
self.name = name
|
|
35
|
+
self.channels = channels
|
|
36
|
+
self.channel_by_fqn = {channel_fqn(c): i for i, c in enumerate(channels)}
|
|
37
|
+
|
|
38
|
+
def as_pb(self, klass: Type[FlowConfigPb]) -> FlowConfigPb:
|
|
39
|
+
return klass(
|
|
40
|
+
name=self.name,
|
|
41
|
+
channels=[conf.as_pb(ChannelConfigPb) for conf in self.channels],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_pb(cls, message: FlowConfigPb) -> Self:
|
|
46
|
+
return cls(
|
|
47
|
+
name=message.name,
|
|
48
|
+
channels=[ChannelConfig.from_pb(c) for c in message.channels],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Flow(TypedDict):
|
|
53
|
+
"""
|
|
54
|
+
Represents a single flow that will be sent to Sift. Because this class uses `sift_py.ingestion.channel.ChannelValue`
|
|
55
|
+
which is a fully qualified channel value, a specific ordering of items in `channel_values` is not required. If a
|
|
56
|
+
particular flow has 5 channels, it is okay to send only data for 3 channels using this class.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
flow_name: str
|
|
60
|
+
timestamp: datetime
|
|
61
|
+
channel_values: List[ChannelValue]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class FlowOrderedChannelValues(TypedDict):
|
|
65
|
+
"""
|
|
66
|
+
Represents a single flow that will be sent to Sift. Unlike `sift_py.ingestion.flow.Flow`, this class requires
|
|
67
|
+
that the ordering of channel values in `channel_values` match what the flow associated with `flow_name` expects.
|
|
68
|
+
If a channel doesn't have particular data to send for a particular time, `sift_py.ingestion.channel.empty_value` should be used
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
flow_name: str
|
|
72
|
+
timestamp: datetime
|
|
73
|
+
channel_values: List[IngestWithConfigDataChannelValue]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from typing import Callable, Dict, Iterator, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
from typing_extensions import Self, TypeAlias
|
|
7
|
+
|
|
8
|
+
from sift_py.grpc.transport import SiftChannel
|
|
9
|
+
from sift_py.ingestion.config.telemetry import TelemetryConfig
|
|
10
|
+
from sift_py.ingestion.service import IngestionService
|
|
11
|
+
|
|
12
|
+
IngestionServiceBuilder: TypeAlias = Callable[[SiftChannel], IngestionService]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IngestionServicesManager:
|
|
16
|
+
"""
|
|
17
|
+
Allows for the initialization of multiple instances of `sift_py.ingestion.service.IngestionService` from
|
|
18
|
+
either telemetry configs or builders under a single wrapper class that assists in managing data-ingestion
|
|
19
|
+
for multiple telemetry configs.
|
|
20
|
+
|
|
21
|
+
The initializer of this class can be used directly, but prefer to use either `from_builders` or `from_telemetry_configs`.
|
|
22
|
+
Prefer to use `from_builders` if you have custom options that you want to provide to `sift_py.ingestion.service.IngestionService.__init__`.
|
|
23
|
+
|
|
24
|
+
Example usage:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
manager = IngestionServicesManager.from_telementry_configs(grpc_channel, {
|
|
28
|
+
"config_a": config_a,
|
|
29
|
+
"config_b": config_b,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
with manager.ingestion_service("config_a") as config_a:
|
|
33
|
+
config_a.try_ingest_flow(...)
|
|
34
|
+
|
|
35
|
+
with manager.ingestion_service("config_b") as config_b:
|
|
36
|
+
config_b.try_ingest_flow(...)
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_transport_channel: SiftChannel
|
|
41
|
+
_ingestion_services: Dict[str, IngestionService]
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self, transport_channel: SiftChannel, ingestion_services: Dict[str, IngestionService]
|
|
45
|
+
):
|
|
46
|
+
self._transport_channel = transport_channel
|
|
47
|
+
self._ingestion_services = ingestion_services
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_builders(
|
|
51
|
+
cls, channel: SiftChannel, builders: Dict[str, IngestionServiceBuilder]
|
|
52
|
+
) -> Self:
|
|
53
|
+
return cls(
|
|
54
|
+
transport_channel=channel,
|
|
55
|
+
ingestion_services={key: builder(channel) for key, builder in builders.items()},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_telemetry_configs(
|
|
60
|
+
cls, channel: SiftChannel, telemetry_configs: Dict[str, TelemetryConfig]
|
|
61
|
+
) -> Self:
|
|
62
|
+
return cls(
|
|
63
|
+
transport_channel=channel,
|
|
64
|
+
ingestion_services={
|
|
65
|
+
key: IngestionService(channel, config) for key, config in telemetry_configs.items()
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def get_ingestion_service_by_identifier(self, identifier: str) -> Optional[IngestionService]:
|
|
70
|
+
return self._ingestion_services.get(identifier)
|
|
71
|
+
|
|
72
|
+
def __getitem__(self, identifier: str) -> Optional[IngestionService]:
|
|
73
|
+
return self.get_ingestion_service_by_identifier(identifier)
|
|
74
|
+
|
|
75
|
+
@contextmanager
|
|
76
|
+
def ingestion_service(self, identifier: str) -> Iterator[IngestionService]:
|
|
77
|
+
ingestion_service = self[identifier]
|
|
78
|
+
|
|
79
|
+
if ingestion_service is None:
|
|
80
|
+
raise IngestionServiceManagerError(
|
|
81
|
+
f"An ingestion service is not configured for the identifier '{identifier}'."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
yield ingestion_service
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class IngestionServiceManagerError(Exception):
|
|
88
|
+
def __init__(self, msg: str):
|
|
89
|
+
return super().__init__(msg)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TelemetryConfigByIdentifierMap(TypedDict):
|
|
93
|
+
identifier: str
|
|
94
|
+
telemetry_config: TelemetryConfig
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class IngestionConfigServiceBuilderIdentifierMap(TypedDict):
|
|
98
|
+
identifier: str
|
|
99
|
+
builder: IngestionServiceBuilder
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import sift_py.rule.config as rule_config
|
|
2
|
+
|
|
3
|
+
RuleConfig = rule_config.RuleConfig
|
|
4
|
+
RuleAction = rule_config.RuleAction
|
|
5
|
+
RuleActionCreateDataReviewAnnotation = rule_config.RuleActionCreateDataReviewAnnotation
|
|
6
|
+
RuleActionCreatePhaseAnnotation = rule_config.RuleActionCreatePhaseAnnotation
|
|
7
|
+
RuleActionAnnotationKind = rule_config.RuleActionAnnotationKind
|
|
8
|
+
RuleActionKind = rule_config.RuleActionKind
|
|
9
|
+
RuleActionKindStrRep = rule_config.RuleActionKindStrRep
|
|
10
|
+
ExpressionChannelReference = rule_config.ExpressionChannelReference
|
|
11
|
+
ExpressionChannelReferenceChannelConfig = rule_config.ExpressionChannelReferenceChannelConfig
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from sift.ingest.v1.ingest_pb2 import (
|
|
7
|
+
IngestWithConfigDataChannelValue,
|
|
8
|
+
IngestWithConfigDataStreamRequest,
|
|
9
|
+
)
|
|
10
|
+
from sift.ingestion_configs.v1.ingestion_configs_pb2 import IngestionConfig
|
|
11
|
+
|
|
12
|
+
from sift_py.grpc.transport import SiftChannel
|
|
13
|
+
from sift_py.ingestion._internal.ingest import _IngestionServiceImpl
|
|
14
|
+
from sift_py.ingestion.buffer import BufferedIngestionService, OnErrorCallback
|
|
15
|
+
from sift_py.ingestion.channel import ChannelValue
|
|
16
|
+
from sift_py.ingestion.config.telemetry import TelemetryConfig
|
|
17
|
+
from sift_py.ingestion.flow import Flow, FlowConfig, FlowOrderedChannelValues
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class IngestionService(_IngestionServiceImpl):
|
|
21
|
+
"""
|
|
22
|
+
A fully configured service that, when instantiated, is ready to start ingesting data.
|
|
23
|
+
|
|
24
|
+
- `transport_channel`: A gRPC transport channel. Prefer to use `SiftChannel`.
|
|
25
|
+
- `ingestion_config`: The underlying strongly-typed ingestion config. Users of this service don't need to be concerned with this.
|
|
26
|
+
- `asset_name`: The name of the asset to telemeter.
|
|
27
|
+
- `flow_configs_by_name`: A mapping of flow config name to the actual flow config.
|
|
28
|
+
- `run_id`: The ID of the optional run to associated ingested data with.
|
|
29
|
+
- `organization_id`: ID of the organization of the user.
|
|
30
|
+
- `overwrite_rules`:
|
|
31
|
+
If there are rules in Sift that aren't found in the local telemetry config, then initializing
|
|
32
|
+
an `IngestionService` will raise an exception advising the user to update their telemetry config
|
|
33
|
+
with the missing rule before proceeding. Setting this field to `True` replace all rules currently
|
|
34
|
+
in Sift with the rules in the telemetry config.
|
|
35
|
+
- `end_stream_on_error`:
|
|
36
|
+
By default any errors that may occur during ingestion API-side are produced asynchronously and ingestion
|
|
37
|
+
won't be interrupted. The errors produced are surfaced on the user errors page. Setting this field to `True`
|
|
38
|
+
will ensure that any errors that occur during ingestion is returned immediately, terminating the stream. This
|
|
39
|
+
is useful for debugging purposes.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
transport_channel: SiftChannel
|
|
43
|
+
ingestion_config: IngestionConfig
|
|
44
|
+
asset_name: str
|
|
45
|
+
flow_configs_by_name: Dict[str, FlowConfig]
|
|
46
|
+
run_id: Optional[str]
|
|
47
|
+
organization_id: Optional[str]
|
|
48
|
+
end_stream_on_error: bool
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
channel: SiftChannel,
|
|
53
|
+
config: TelemetryConfig,
|
|
54
|
+
run_id: Optional[str] = None,
|
|
55
|
+
overwrite_rules: bool = False,
|
|
56
|
+
end_stream_on_error: bool = False,
|
|
57
|
+
):
|
|
58
|
+
super().__init__(channel, config, run_id, overwrite_rules, end_stream_on_error)
|
|
59
|
+
|
|
60
|
+
def ingest(self, *requests: IngestWithConfigDataStreamRequest):
|
|
61
|
+
"""
|
|
62
|
+
This method performs the actual data ingestion given a list of data ingestion requests.
|
|
63
|
+
"""
|
|
64
|
+
super().ingest(*requests)
|
|
65
|
+
|
|
66
|
+
def attach_run(
|
|
67
|
+
self,
|
|
68
|
+
channel: SiftChannel,
|
|
69
|
+
run_name: str,
|
|
70
|
+
description: Optional[str] = None,
|
|
71
|
+
organization_id: Optional[str] = None,
|
|
72
|
+
tags: Optional[List[str]] = None,
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Retrieve an existing run or create one to use during this period of ingestion.
|
|
76
|
+
"""
|
|
77
|
+
super().attach_run(channel, run_name, description, organization_id, tags)
|
|
78
|
+
|
|
79
|
+
def detach_run(self):
|
|
80
|
+
"""
|
|
81
|
+
Detach run from this period of ingestion. Subsequent data ingested won't be associated with
|
|
82
|
+
the run being detached.
|
|
83
|
+
"""
|
|
84
|
+
super().detach_run()
|
|
85
|
+
|
|
86
|
+
def try_create_ingestion_request(
|
|
87
|
+
self,
|
|
88
|
+
flow_name: str,
|
|
89
|
+
timestamp: datetime,
|
|
90
|
+
channel_values: List[ChannelValue],
|
|
91
|
+
) -> IngestWithConfigDataStreamRequest:
|
|
92
|
+
"""
|
|
93
|
+
Creates an `IngestWithConfigDataStreamRequest`, i.e. a flow, given a `flow_name` and a
|
|
94
|
+
list of `ChannelValue` objects. Channels that appear in the flow config but not in the
|
|
95
|
+
`channel_values` will be assigned an empty value.
|
|
96
|
+
|
|
97
|
+
This function will perform validation checks to ensure that the values provided in the dictionary; this
|
|
98
|
+
includes:
|
|
99
|
+
- Making sure the flow exists
|
|
100
|
+
- Making sure that the there are no unexpected channels provided for the given flow
|
|
101
|
+
- Making sure the channel value is the expected type
|
|
102
|
+
- Making sure that the timestamp is in UTC
|
|
103
|
+
- Making sure channels that belong to a component have the 'component' field for the channel value
|
|
104
|
+
|
|
105
|
+
If any of the above validations fail then a `IngestionValidationError` will be raised.
|
|
106
|
+
|
|
107
|
+
If for performance reasons you'd prefer to skip the validation checks, or perhaps you did the
|
|
108
|
+
validations on your own, prefer to use `create_ingestion_request`. Any errors that occur during
|
|
109
|
+
ingestion will be handled by the Sift API.
|
|
110
|
+
"""
|
|
111
|
+
return super().try_create_ingestion_request(flow_name, timestamp, channel_values)
|
|
112
|
+
|
|
113
|
+
def create_ingestion_request(
|
|
114
|
+
self,
|
|
115
|
+
flow_name: str,
|
|
116
|
+
timestamp: datetime,
|
|
117
|
+
channel_values: List[IngestWithConfigDataChannelValue],
|
|
118
|
+
) -> IngestWithConfigDataStreamRequest:
|
|
119
|
+
"""
|
|
120
|
+
Unlike `try_create_ingestion_request`, this skips argument validations. Useful for when user has already done their own
|
|
121
|
+
argument validation or if they require low-latency execution time client-side.
|
|
122
|
+
|
|
123
|
+
If there are errors that occur during ingestion and the `end_stream_on_error` attribute is set to `False`,
|
|
124
|
+
the data ingestion stream will skip over them and errors instead will be produced asynchronously and become
|
|
125
|
+
available in the UI application in the errors page. If `end_stream_on_error` is set to `True`, then the
|
|
126
|
+
data ingestion stream will be terminated if an error is encountered during ingestion.
|
|
127
|
+
|
|
128
|
+
These are some things to look out for when using this method instead of `try_create_ingestion_request`:
|
|
129
|
+
- Values in `channel_values` must appear in the same order its corresponding channel appears in the flow config
|
|
130
|
+
associated with the `flow_name`.
|
|
131
|
+
- The length of `channel_values` is expected to match the length of the channel configs list of the flow config
|
|
132
|
+
associated with `flow_name`. `sift_py.ingestion.channel.empty_value()` may be used if you require empty values.
|
|
133
|
+
- The `timestamp` must be in UTC.
|
|
134
|
+
"""
|
|
135
|
+
return super().create_ingestion_request(flow_name, timestamp, channel_values)
|
|
136
|
+
|
|
137
|
+
def ingest_flows(self, *flows: FlowOrderedChannelValues):
|
|
138
|
+
"""
|
|
139
|
+
Combines the requests creation step and ingestion into a single call.
|
|
140
|
+
See `create_ingestion_request` for information about how client-side validations are handled.
|
|
141
|
+
"""
|
|
142
|
+
return super().ingest_flows(*flows)
|
|
143
|
+
|
|
144
|
+
def try_ingest_flows(self, *flows: Flow):
|
|
145
|
+
"""
|
|
146
|
+
Combines the requests creation step and ingestion into a single call.
|
|
147
|
+
See `try_create_ingestion_request` for information about how client-side validations are handled.
|
|
148
|
+
"""
|
|
149
|
+
return super().try_ingest_flows(*flows)
|
|
150
|
+
|
|
151
|
+
def buffered_ingestion(
|
|
152
|
+
self,
|
|
153
|
+
buffer_size: Optional[int] = None,
|
|
154
|
+
flush_interval_sec: Optional[float] = None,
|
|
155
|
+
on_error: Optional[OnErrorCallback] = None,
|
|
156
|
+
) -> BufferedIngestionService:
|
|
157
|
+
"""
|
|
158
|
+
This method automates buffering requests and streams them in batches. It is recommended to be used
|
|
159
|
+
in a with-block. Failure to put this in a with-block may result in some data not being ingested unless
|
|
160
|
+
the caller explicitly calls `sift_py.ingestion.buffer.BufferedIngestionService.flush` before the returned
|
|
161
|
+
instance of `sift_py.ingestion.buffer.BufferedIngestionService` goes out of scope. Once the with-block
|
|
162
|
+
is exited then a final call to the aforementioned `flush` method will be made to ingest the remaining data.
|
|
163
|
+
|
|
164
|
+
Buffered ingestion works by automatically flushing and ingesting data into Sift whenever the buffer is filled.
|
|
165
|
+
The size of the buffer is configured via the `buffer_size` argument and defaults to `sift_py.ingestion.buffer.DEFAULT_BUFFER_SIZE`.
|
|
166
|
+
|
|
167
|
+
It is also possible to configure buffered ingestion to periodically flush the buffer regardless of whether or not the buffer
|
|
168
|
+
is filled. The interval between flushes is set via the `flush_interval_sec` argument which is the number of seconds between each flush.
|
|
169
|
+
If a flush were to occur due to the buffer being filled, then the timer will restart. If `flush_interval_sec` is `None`, then flushes will only
|
|
170
|
+
occur once the buffer is filled and at the end of the scope of the with-block.
|
|
171
|
+
|
|
172
|
+
If an error were to occur that would cause the context manager to call `__exit__`, one last attempt to flush the buffer will be made
|
|
173
|
+
before the error is re-raised for the caller to handle. If the caller would instead like to customize `__exit__` behavior in the case
|
|
174
|
+
of an error, they can make use of the `on_error` argument whose type signature is a function where the first argument is the error,
|
|
175
|
+
the second is the buffer containing the uningested request, and the third argument being a function where, when called, will attempt
|
|
176
|
+
to flush the buffer.
|
|
177
|
+
|
|
178
|
+
Example usage:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
# With client-side validations
|
|
182
|
+
with ingestion_service.buffered_ingestion() as buffered_ingestion:
|
|
183
|
+
for _ in range(10_000):
|
|
184
|
+
buffered_ingestion.try_ingest_flows({
|
|
185
|
+
"flow_name": "readings",
|
|
186
|
+
"timestamp": datetime.now(timezone.utc),
|
|
187
|
+
"channel_values": [
|
|
188
|
+
{
|
|
189
|
+
"channel_name": "my-channel",
|
|
190
|
+
],
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
# Without client-side validations and a custom buffer size
|
|
194
|
+
with ingestion_service.buffered_ingestion(2_000) as buffered_ingestion:
|
|
195
|
+
for _ in range(6_000):
|
|
196
|
+
buffered_ingestion.ingest_flows({
|
|
197
|
+
"flow_name": "readings",
|
|
198
|
+
"timestamp": datetime.now(timezone.utc),
|
|
199
|
+
"channel_values": [double_value(3)]
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
# With default buffer size and periodic flushes of 3.2 seconds
|
|
203
|
+
with ingestion_service.buffered_ingestion(flush_interval_sec=3.2) as buffered_ingestion:
|
|
204
|
+
for _ in range(6_000):
|
|
205
|
+
buffered_ingestion.ingest_flows({
|
|
206
|
+
"flow_name": "readings",
|
|
207
|
+
"timestamp": datetime.now(timezone.utc),
|
|
208
|
+
"channel_values": [double_value(3)]
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
# Custom code to run when error
|
|
212
|
+
def on_error_calback(err, buffer, flush):
|
|
213
|
+
# Save contents of buffer to disk
|
|
214
|
+
...
|
|
215
|
+
# Try once more to flush the buffer
|
|
216
|
+
flush()
|
|
217
|
+
|
|
218
|
+
with ingestion_service.buffered_ingestion(on_error=on_error_calback) as buffered_ingestion:
|
|
219
|
+
...
|
|
220
|
+
```
|
|
221
|
+
"""
|
|
222
|
+
return BufferedIngestionService(self, buffer_size, flush_interval_sec, on_error)
|
|
223
|
+
|
|
224
|
+
def create_flow(self, flow_config: FlowConfig):
|
|
225
|
+
"""
|
|
226
|
+
Like `try_create_new_flow` but will automatically overwrite any existing flow config with `flow_config` if they
|
|
227
|
+
share the same name. If you'd an exception to be raise in the case of a name collision then see `try_create_new_flow`.
|
|
228
|
+
"""
|
|
229
|
+
super().create_flow(flow_config)
|
|
230
|
+
|
|
231
|
+
def try_create_flow(self, flow_config: FlowConfig):
|
|
232
|
+
"""
|
|
233
|
+
Tries to create a new flow at runtime. Will raise an `IngestionValidationError` if there already exists
|
|
234
|
+
a flow with the name of the `flow_config` argument. If you'd like to overwrite any flow configs with that
|
|
235
|
+
have the same name as the provided `flow_config`, then see `create_new_flow`.
|
|
236
|
+
"""
|
|
237
|
+
super().try_create_flow(flow_config)
|
sift_py/py.typed
ADDED
|
File without changes
|
|
File without changes
|