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,350 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
from warnings import warn
|
|
7
|
+
|
|
8
|
+
from google.protobuf.timestamp_pb2 import Timestamp
|
|
9
|
+
from sift.ingest.v1.ingest_pb2 import (
|
|
10
|
+
IngestWithConfigDataChannelValue,
|
|
11
|
+
IngestWithConfigDataStreamRequest,
|
|
12
|
+
)
|
|
13
|
+
from sift.ingest.v1.ingest_pb2_grpc import IngestServiceStub
|
|
14
|
+
from sift.ingestion_configs.v1.ingestion_configs_pb2 import ChannelConfig as ChannelConfigPb
|
|
15
|
+
from sift.ingestion_configs.v1.ingestion_configs_pb2 import IngestionConfig
|
|
16
|
+
|
|
17
|
+
from sift_py.grpc.transport import SiftChannel
|
|
18
|
+
from sift_py.ingestion._internal.error import IngestionValidationError
|
|
19
|
+
from sift_py.ingestion._internal.ingestion_config import (
|
|
20
|
+
create_flow_configs,
|
|
21
|
+
create_ingestion_config,
|
|
22
|
+
get_ingestion_config_by_client_key,
|
|
23
|
+
get_ingestion_config_flows,
|
|
24
|
+
)
|
|
25
|
+
from sift_py.ingestion._internal.run import create_run, get_run_id_by_name
|
|
26
|
+
from sift_py.ingestion.channel import (
|
|
27
|
+
ChannelConfig,
|
|
28
|
+
ChannelValue,
|
|
29
|
+
channel_fqn,
|
|
30
|
+
empty_value,
|
|
31
|
+
is_data_type,
|
|
32
|
+
)
|
|
33
|
+
from sift_py.ingestion.config.telemetry import TelemetryConfig
|
|
34
|
+
from sift_py.ingestion.flow import Flow, FlowConfig, FlowOrderedChannelValues
|
|
35
|
+
from sift_py.ingestion.rule.config import RuleConfig
|
|
36
|
+
from sift_py.rule.service import RuleService
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _IngestionServiceImpl:
|
|
40
|
+
transport_channel: SiftChannel
|
|
41
|
+
ingestion_config: IngestionConfig
|
|
42
|
+
asset_name: str
|
|
43
|
+
flow_configs_by_name: Dict[str, FlowConfig]
|
|
44
|
+
rules: List[RuleConfig]
|
|
45
|
+
run_id: Optional[str]
|
|
46
|
+
organization_id: Optional[str]
|
|
47
|
+
end_stream_on_error: bool
|
|
48
|
+
|
|
49
|
+
ingest_service_stub: IngestServiceStub
|
|
50
|
+
rule_service: RuleService
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
channel: SiftChannel,
|
|
55
|
+
config: TelemetryConfig,
|
|
56
|
+
run_id: Optional[str] = None,
|
|
57
|
+
overwrite_rules: bool = False,
|
|
58
|
+
end_stream_on_error: bool = False,
|
|
59
|
+
):
|
|
60
|
+
ingestion_config = self.__class__._get_or_create_ingestion_config(channel, config)
|
|
61
|
+
|
|
62
|
+
if overwrite_rules:
|
|
63
|
+
warn(
|
|
64
|
+
"The 'overwrite_rules' argument is deprecated and will be removed in release 0.4.0."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self.rule_service = RuleService(channel)
|
|
68
|
+
if config.rules:
|
|
69
|
+
for rule in config.rules:
|
|
70
|
+
if config.asset_name not in rule.asset_names:
|
|
71
|
+
rule.asset_names.append(config.asset_name)
|
|
72
|
+
self.rule_service.create_or_update_rules(config.rules)
|
|
73
|
+
|
|
74
|
+
self.rules = config.rules
|
|
75
|
+
self.ingestion_config = ingestion_config
|
|
76
|
+
self.asset_name = config.asset_name
|
|
77
|
+
self.transport_channel = channel
|
|
78
|
+
self.run_id = run_id
|
|
79
|
+
self.organization_id = config.organization_id
|
|
80
|
+
self.end_stream_on_error = end_stream_on_error
|
|
81
|
+
self.flow_configs_by_name = {flow.name: flow for flow in config.flows}
|
|
82
|
+
self.ingest_service_stub = IngestServiceStub(channel)
|
|
83
|
+
|
|
84
|
+
def ingest(self, *requests: IngestWithConfigDataStreamRequest):
|
|
85
|
+
"""
|
|
86
|
+
Perform data ingestion.
|
|
87
|
+
"""
|
|
88
|
+
self.ingest_service_stub.IngestWithConfigDataStream(iter(requests))
|
|
89
|
+
|
|
90
|
+
def ingest_flows(self, *flows: FlowOrderedChannelValues):
|
|
91
|
+
"""
|
|
92
|
+
Combines the requests creation step and ingestion into a single call.
|
|
93
|
+
See `create_ingestion_request` for information about how client-side validations are handled.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
requests = []
|
|
97
|
+
for flow in flows:
|
|
98
|
+
flow_name = flow["flow_name"]
|
|
99
|
+
timestamp = flow["timestamp"]
|
|
100
|
+
channel_values = flow["channel_values"]
|
|
101
|
+
req = self.create_ingestion_request(flow_name, timestamp, channel_values)
|
|
102
|
+
requests.append(req)
|
|
103
|
+
|
|
104
|
+
self.ingest_service_stub.IngestWithConfigDataStream(iter(requests))
|
|
105
|
+
|
|
106
|
+
def try_ingest_flows(self, *flows: Flow):
|
|
107
|
+
"""
|
|
108
|
+
Combines the requests creation step and ingestion into a single call.
|
|
109
|
+
See `try_create_ingestion_request` for information about how client-side validations are handled.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
requests = []
|
|
113
|
+
for flow in flows:
|
|
114
|
+
flow_name = flow["flow_name"]
|
|
115
|
+
timestamp = flow["timestamp"]
|
|
116
|
+
channel_values = flow["channel_values"]
|
|
117
|
+
req = self.try_create_ingestion_request(flow_name, timestamp, channel_values)
|
|
118
|
+
requests.append(req)
|
|
119
|
+
|
|
120
|
+
self.ingest_service_stub.IngestWithConfigDataStream(iter(requests))
|
|
121
|
+
|
|
122
|
+
def attach_run(
|
|
123
|
+
self,
|
|
124
|
+
channel: SiftChannel,
|
|
125
|
+
run_name: str,
|
|
126
|
+
description: Optional[str] = None,
|
|
127
|
+
organization_id: Optional[str] = None,
|
|
128
|
+
tags: Optional[List[str]] = None,
|
|
129
|
+
):
|
|
130
|
+
"""
|
|
131
|
+
Retrieve an existing run or create one to use during this period of ingestion.
|
|
132
|
+
"""
|
|
133
|
+
run_id = get_run_id_by_name(channel, run_name)
|
|
134
|
+
|
|
135
|
+
if run_id is not None:
|
|
136
|
+
self.run_id = run_id
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
self.run_id = create_run(
|
|
140
|
+
channel=channel,
|
|
141
|
+
run_name=run_name,
|
|
142
|
+
description=description or "",
|
|
143
|
+
organization_id=organization_id or "",
|
|
144
|
+
tags=tags or [],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def detach_run(self):
|
|
148
|
+
"""
|
|
149
|
+
Detach run from this period of ingestion. Subsequent data ingested won't be associated with
|
|
150
|
+
the run being detached.
|
|
151
|
+
"""
|
|
152
|
+
self.run_id = None
|
|
153
|
+
|
|
154
|
+
def try_create_ingestion_request(
|
|
155
|
+
self,
|
|
156
|
+
flow_name: str,
|
|
157
|
+
timestamp: datetime,
|
|
158
|
+
channel_values: List[ChannelValue],
|
|
159
|
+
) -> IngestWithConfigDataStreamRequest:
|
|
160
|
+
"""
|
|
161
|
+
Creates an ingestion request for a flow that must exist in `flow_configs_by_name`. This method
|
|
162
|
+
performs a series of client-side validations and will return a `IngestionValidationError` if any validations fail.
|
|
163
|
+
"""
|
|
164
|
+
flow_config = self.flow_configs_by_name.get(flow_name)
|
|
165
|
+
|
|
166
|
+
if flow_config is None:
|
|
167
|
+
raise IngestionValidationError(
|
|
168
|
+
f"A flow config of name '{flow_name}' could not be found."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
channel_values_by_fqn: Dict[str, ChannelValue] = {}
|
|
172
|
+
|
|
173
|
+
for channel_value in channel_values:
|
|
174
|
+
fqn = channel_fqn(channel_value)
|
|
175
|
+
|
|
176
|
+
if channel_values_by_fqn.get(fqn, None) is None:
|
|
177
|
+
channel_values_by_fqn[fqn] = channel_value
|
|
178
|
+
else:
|
|
179
|
+
raise IngestionValidationError(f"Encountered multiple values for {fqn}")
|
|
180
|
+
|
|
181
|
+
values: List[IngestWithConfigDataChannelValue] = []
|
|
182
|
+
|
|
183
|
+
for channel in flow_config.channels:
|
|
184
|
+
fqn = channel_fqn(channel)
|
|
185
|
+
channel_val: Optional[ChannelValue] = channel_values_by_fqn.pop(fqn, None)
|
|
186
|
+
|
|
187
|
+
if channel_val is None:
|
|
188
|
+
values.append(empty_value())
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
value = channel_val["value"]
|
|
192
|
+
|
|
193
|
+
if is_data_type(value, channel.data_type):
|
|
194
|
+
values.append(value)
|
|
195
|
+
else:
|
|
196
|
+
raise IngestionValidationError(
|
|
197
|
+
f"Expected value for `{channel.name}` to be a '{channel.data_type}'."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if len(channel_values_by_fqn) > 0:
|
|
201
|
+
unexpected_channels = [name for name in channel_values_by_fqn.keys()]
|
|
202
|
+
raise IngestionValidationError(
|
|
203
|
+
f"Unexpected channel(s) for flow '{flow_name}': {unexpected_channels}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if timestamp.tzname() != "UTC":
|
|
207
|
+
raise IngestionValidationError(
|
|
208
|
+
f"Expected 'timestamp' to be in UTC but it is in {timestamp.tzname()}."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
timestamp_pb = Timestamp()
|
|
212
|
+
timestamp_pb.FromDatetime(timestamp)
|
|
213
|
+
|
|
214
|
+
return self.create_ingestion_request(flow_name, timestamp, values)
|
|
215
|
+
|
|
216
|
+
def create_ingestion_request(
|
|
217
|
+
self,
|
|
218
|
+
flow_name: str,
|
|
219
|
+
timestamp: datetime,
|
|
220
|
+
channel_values: List[IngestWithConfigDataChannelValue],
|
|
221
|
+
) -> IngestWithConfigDataStreamRequest:
|
|
222
|
+
"""
|
|
223
|
+
Creates an ingestion request for a flow that must exist in `flow_configs_by_name`. This method
|
|
224
|
+
does not do any sort of client-side validation and is recommended to use if performance is required.
|
|
225
|
+
"""
|
|
226
|
+
timestamp_pb = Timestamp()
|
|
227
|
+
timestamp_pb.FromDatetime(timestamp)
|
|
228
|
+
|
|
229
|
+
return IngestWithConfigDataStreamRequest(
|
|
230
|
+
ingestion_config_id=self.ingestion_config.ingestion_config_id,
|
|
231
|
+
flow=flow_name,
|
|
232
|
+
timestamp=timestamp_pb,
|
|
233
|
+
channel_values=channel_values,
|
|
234
|
+
run_id=self.run_id or "",
|
|
235
|
+
organization_id=self.organization_id or "",
|
|
236
|
+
end_stream_on_validation_error=self.end_stream_on_error,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def try_create_flow(self, flow_config: FlowConfig):
|
|
240
|
+
"""
|
|
241
|
+
Tries to create a new flow at runtime. Will raise an `IngestionValidationError` if there already exists
|
|
242
|
+
a flow with the name of the `flow_config` argument. If you'd like to overwrite any flow configs with that
|
|
243
|
+
have the same name as the provided `flow_config`, then see `create_flow`.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
if flow_config.name in self.flow_configs_by_name:
|
|
247
|
+
raise IngestionValidationError(
|
|
248
|
+
f"There is already a flow with name '{flow_config.name}'."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
create_flow_configs(
|
|
252
|
+
self.transport_channel,
|
|
253
|
+
self.ingestion_config.ingestion_config_id,
|
|
254
|
+
[flow_config],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
self.flow_configs_by_name[flow_config.name] = flow_config
|
|
258
|
+
|
|
259
|
+
def create_flow(self, flow_config: FlowConfig):
|
|
260
|
+
"""
|
|
261
|
+
Like `try_create_flow` but will automatically overwrite any existing flow config with `flow_config` if they
|
|
262
|
+
share the same name. If you'd an exception to be raise in the case of a name collision then see `try_create_flow`.
|
|
263
|
+
"""
|
|
264
|
+
create_flow_configs(
|
|
265
|
+
self.transport_channel,
|
|
266
|
+
self.ingestion_config.ingestion_config_id,
|
|
267
|
+
[flow_config],
|
|
268
|
+
)
|
|
269
|
+
self.flow_configs_by_name[flow_config.name] = flow_config
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def _update_flow_configs(
|
|
273
|
+
channel: SiftChannel, ingestion_config_id: str, telemetry_config: TelemetryConfig
|
|
274
|
+
):
|
|
275
|
+
"""
|
|
276
|
+
Compares local flows from a telemetry config with the flows registered in Sift. If a local flow
|
|
277
|
+
contains channels that isn't in Sift then this will create a new flow containing those channels.
|
|
278
|
+
"""
|
|
279
|
+
config_flows = telemetry_config.flows
|
|
280
|
+
|
|
281
|
+
if len(config_flows) == 0:
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
sift_flows = get_ingestion_config_flows(channel, ingestion_config_id)
|
|
285
|
+
|
|
286
|
+
sift_flow_indices_by_name = {flow.name: i for i, flow in enumerate(sift_flows)}
|
|
287
|
+
|
|
288
|
+
flows_to_create: List[FlowConfig] = []
|
|
289
|
+
|
|
290
|
+
# We can have multiple channels of the same name but different data-type. This will create a completely unique channel
|
|
291
|
+
# identifier by creating a composite key of the fully qualified channel name with the channel's data-type.
|
|
292
|
+
sift_channel_identifier: Callable[[ChannelConfigPb], str] = (
|
|
293
|
+
lambda x: f"{channel_fqn(x)}.{x.data_type}"
|
|
294
|
+
)
|
|
295
|
+
config_channel_identifier: Callable[[ChannelConfig], str] = (
|
|
296
|
+
lambda x: f"{channel_fqn(x)}.{x.data_type.value}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
for config_flow in config_flows:
|
|
300
|
+
sift_flow_index = sift_flow_indices_by_name.get(config_flow.name)
|
|
301
|
+
|
|
302
|
+
# There isn't a flow in Sift for this config flow so we'll create it.
|
|
303
|
+
if sift_flow_index is None:
|
|
304
|
+
flows_to_create.append(config_flow)
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
# There is a flow in Sift with the name of the config flow. We'll
|
|
308
|
+
# compare the channels in the config flow with the sift flow and
|
|
309
|
+
# see if there's a difference. If there is we'll create a new flow.
|
|
310
|
+
sift_flow = sift_flows[sift_flow_index]
|
|
311
|
+
|
|
312
|
+
sift_channel_identifiers = {
|
|
313
|
+
sift_channel_identifier(channel) for channel in sift_flow.channels
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for config_channel in config_flow.channels:
|
|
317
|
+
# Found a channel for this flow that doesn't exist in Sift based on channel
|
|
318
|
+
# fully-qualified name and data-type. Create a new flow.
|
|
319
|
+
if not config_channel_identifier(config_channel) in sift_channel_identifiers:
|
|
320
|
+
flows_to_create.append(config_flow)
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
if len(flows_to_create) > 0:
|
|
324
|
+
create_flow_configs(channel, ingestion_config_id, flows_to_create)
|
|
325
|
+
|
|
326
|
+
@classmethod
|
|
327
|
+
def _get_or_create_ingestion_config(
|
|
328
|
+
cls, channel: SiftChannel, config: TelemetryConfig
|
|
329
|
+
) -> IngestionConfig:
|
|
330
|
+
"""
|
|
331
|
+
Retrieves an existing ingestion config or creates a new one. If an existing ingestion config is fetched,
|
|
332
|
+
then flows may be updated to reflect any changes that may have occured in the telemetry config.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
ingestion_config = get_ingestion_config_by_client_key(channel, config.ingestion_client_key)
|
|
336
|
+
|
|
337
|
+
# Exiting ingestion config.. update flows if necessary
|
|
338
|
+
if ingestion_config is not None:
|
|
339
|
+
cls._update_flow_configs(channel, ingestion_config.ingestion_config_id, config)
|
|
340
|
+
return ingestion_config
|
|
341
|
+
|
|
342
|
+
ingestion_config = create_ingestion_config(
|
|
343
|
+
channel,
|
|
344
|
+
config.asset_name,
|
|
345
|
+
config.flows,
|
|
346
|
+
config.ingestion_client_key,
|
|
347
|
+
config.organization_id,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return ingestion_config
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from pytest_mock import MockFixture
|
|
7
|
+
from sift.ingestion_configs.v1.ingestion_configs_pb2 import (
|
|
8
|
+
FlowConfig as FlowConfigPb,
|
|
9
|
+
)
|
|
10
|
+
from sift.ingestion_configs.v1.ingestion_configs_pb2 import (
|
|
11
|
+
IngestionConfig as IngestionConfigPb,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
import sift_py.ingestion._internal.ingest
|
|
15
|
+
from sift_py._internal.test_util.channel import MockChannel
|
|
16
|
+
from sift_py._internal.test_util.fn import _mock_path as _mock_path_imp
|
|
17
|
+
from sift_py.ingestion._internal.error import IngestionValidationError
|
|
18
|
+
from sift_py.ingestion._internal.ingest import (
|
|
19
|
+
_IngestionServiceImpl,
|
|
20
|
+
)
|
|
21
|
+
from sift_py.ingestion._internal.ingestion_config import (
|
|
22
|
+
create_flow_configs,
|
|
23
|
+
create_ingestion_config,
|
|
24
|
+
get_ingestion_config_by_client_key,
|
|
25
|
+
get_ingestion_config_flows,
|
|
26
|
+
)
|
|
27
|
+
from sift_py.ingestion.channel import (
|
|
28
|
+
ChannelConfig,
|
|
29
|
+
ChannelDataType,
|
|
30
|
+
double_value,
|
|
31
|
+
int32_value,
|
|
32
|
+
string_value,
|
|
33
|
+
)
|
|
34
|
+
from sift_py.ingestion.config.telemetry import TelemetryConfig
|
|
35
|
+
from sift_py.ingestion.flow import FlowConfig
|
|
36
|
+
from sift_py.ingestion.rule.config import RuleActionCreateDataReviewAnnotation, RuleConfig
|
|
37
|
+
|
|
38
|
+
_mock_path = _mock_path_imp(sift_py.ingestion._internal.ingest)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_ingestion_service_update_flow_configs_updates_flows(mocker: MockFixture):
|
|
42
|
+
"""
|
|
43
|
+
Tests to ensure that if a user adds a new flow to the telemetry config,
|
|
44
|
+
`update_flow_configs` will ensure that it gets created.
|
|
45
|
+
"""
|
|
46
|
+
ingestion_config_id = "ingestion-config-id"
|
|
47
|
+
|
|
48
|
+
flow_a = FlowConfig(
|
|
49
|
+
name="flow_a",
|
|
50
|
+
channels=[
|
|
51
|
+
ChannelConfig(
|
|
52
|
+
name="channel_a",
|
|
53
|
+
data_type=ChannelDataType.DOUBLE,
|
|
54
|
+
),
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
flow_b = FlowConfig(
|
|
59
|
+
name="flow_b",
|
|
60
|
+
channels=[
|
|
61
|
+
ChannelConfig(
|
|
62
|
+
name="channel_b",
|
|
63
|
+
data_type=ChannelDataType.DOUBLE,
|
|
64
|
+
),
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
flows_from_api = [flow_a.as_pb(FlowConfigPb)]
|
|
69
|
+
|
|
70
|
+
telemetry_config = TelemetryConfig(
|
|
71
|
+
asset_name="my-config",
|
|
72
|
+
ingestion_client_key="my-key",
|
|
73
|
+
flows=[flow_a, flow_b],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
mock_get_ingestion_config_flow_names = mocker.patch(_mock_path(get_ingestion_config_flows))
|
|
77
|
+
mock_get_ingestion_config_flow_names.return_value = flows_from_api
|
|
78
|
+
|
|
79
|
+
mock_create_flow_configs = mocker.patch(_mock_path(create_flow_configs))
|
|
80
|
+
mock_create_flow_configs.return_value = None
|
|
81
|
+
|
|
82
|
+
mock_channel = MockChannel()
|
|
83
|
+
_IngestionServiceImpl._update_flow_configs(mock_channel, ingestion_config_id, telemetry_config)
|
|
84
|
+
mock_create_flow_configs.assert_called_once_with(mock_channel, ingestion_config_id, [flow_b])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_ingestion_service_get_or_create_ingestion_config_retrieves_existing(mocker: MockFixture):
|
|
88
|
+
"""
|
|
89
|
+
Ensure that if an ingestion config is queried by client key, a new one is not created.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
mock_ingestion_config = IngestionConfigPb(
|
|
93
|
+
ingestion_config_id="ingestion-config-id",
|
|
94
|
+
asset_id="asset-id",
|
|
95
|
+
client_key="client-key",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
mock_telemetry_config = TelemetryConfig(
|
|
99
|
+
asset_name="asset_name",
|
|
100
|
+
ingestion_client_key=mock_ingestion_config.client_key,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
mock_get_ingestion_config_by_client_key = mocker.patch(
|
|
104
|
+
_mock_path(get_ingestion_config_by_client_key)
|
|
105
|
+
)
|
|
106
|
+
mock_get_ingestion_config_by_client_key.return_value = mock_ingestion_config
|
|
107
|
+
|
|
108
|
+
mock_create_ingestion_config = mocker.patch(_mock_path(create_ingestion_config))
|
|
109
|
+
|
|
110
|
+
mock_channel = MockChannel()
|
|
111
|
+
|
|
112
|
+
ingestion_config = _IngestionServiceImpl._get_or_create_ingestion_config(
|
|
113
|
+
mock_channel,
|
|
114
|
+
mock_telemetry_config,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
mock_create_ingestion_config.assert_not_called()
|
|
118
|
+
|
|
119
|
+
assert ingestion_config.ingestion_config_id == mock_ingestion_config.ingestion_config_id
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_ingestion_service_get_or_create_ingestion_config_create_if_not_exist(mocker: MockFixture):
|
|
123
|
+
"""
|
|
124
|
+
Ensure that if an ingestion config does not exist for a given client key then a new
|
|
125
|
+
ingestion config is created based on the telemetry config.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
mock_ingestion_config = IngestionConfigPb(
|
|
129
|
+
ingestion_config_id="ingestion-config-id",
|
|
130
|
+
asset_id="asset-id",
|
|
131
|
+
client_key="client-key",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
mock_telemetry_config = TelemetryConfig(
|
|
135
|
+
asset_name="asset_name",
|
|
136
|
+
ingestion_client_key=mock_ingestion_config.client_key,
|
|
137
|
+
organization_id="my-org-id",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
mock_get_ingestion_config_by_client_key = mocker.patch(
|
|
141
|
+
_mock_path(get_ingestion_config_by_client_key)
|
|
142
|
+
)
|
|
143
|
+
mock_get_ingestion_config_by_client_key.return_value = None
|
|
144
|
+
|
|
145
|
+
mock_create_ingestion_config = mocker.patch(_mock_path(create_ingestion_config))
|
|
146
|
+
mock_create_ingestion_config.return_value = mock_ingestion_config
|
|
147
|
+
|
|
148
|
+
mock_channel = MockChannel()
|
|
149
|
+
|
|
150
|
+
ingestion_config = _IngestionServiceImpl._get_or_create_ingestion_config(
|
|
151
|
+
mock_channel,
|
|
152
|
+
mock_telemetry_config,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
mock_create_ingestion_config.assert_called_once_with(
|
|
156
|
+
mock_channel,
|
|
157
|
+
mock_telemetry_config.asset_name,
|
|
158
|
+
[],
|
|
159
|
+
mock_ingestion_config.client_key,
|
|
160
|
+
mock_telemetry_config.organization_id,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
assert ingestion_config.ingestion_config_id == mock_ingestion_config.ingestion_config_id
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_ingestion_service_try_create_ingestion_request_validations(mocker: MockFixture):
|
|
167
|
+
"""
|
|
168
|
+
Tests all the different validations that happen when trying to create an ingestion request.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
voltage_channel = ChannelConfig(
|
|
172
|
+
name="voltage",
|
|
173
|
+
component="motor",
|
|
174
|
+
data_type=ChannelDataType.DOUBLE,
|
|
175
|
+
)
|
|
176
|
+
pressure_channel = ChannelConfig(
|
|
177
|
+
name="pressure",
|
|
178
|
+
data_type=ChannelDataType.INT_64,
|
|
179
|
+
)
|
|
180
|
+
logs_channel = ChannelConfig(
|
|
181
|
+
name="logs",
|
|
182
|
+
data_type=ChannelDataType.STRING,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
telemetry_config = TelemetryConfig(
|
|
186
|
+
asset_name="my-asset",
|
|
187
|
+
ingestion_client_key="my-client-key",
|
|
188
|
+
flows=[
|
|
189
|
+
FlowConfig(
|
|
190
|
+
name="reading",
|
|
191
|
+
channels=[voltage_channel, pressure_channel],
|
|
192
|
+
),
|
|
193
|
+
FlowConfig(
|
|
194
|
+
name="pressure",
|
|
195
|
+
channels=[pressure_channel],
|
|
196
|
+
),
|
|
197
|
+
FlowConfig(
|
|
198
|
+
name="log",
|
|
199
|
+
channels=[logs_channel],
|
|
200
|
+
),
|
|
201
|
+
],
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
mock_ingestion_config = IngestionConfigPb(
|
|
205
|
+
ingestion_config_id="ingestion-config-id",
|
|
206
|
+
asset_id="my-asset-id",
|
|
207
|
+
client_key="my-client-key",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
mock_get_or_create_ingestion_config = mocker.patch.object(
|
|
211
|
+
_IngestionServiceImpl, "_get_or_create_ingestion_config"
|
|
212
|
+
)
|
|
213
|
+
mock_get_or_create_ingestion_config.return_value = mock_ingestion_config
|
|
214
|
+
|
|
215
|
+
mock_update_flow_configs = mocker.patch.object(_IngestionServiceImpl, "_update_flow_configs")
|
|
216
|
+
mock_update_flow_configs.return_value = None
|
|
217
|
+
|
|
218
|
+
transport_channel = MockChannel()
|
|
219
|
+
|
|
220
|
+
with mocker.patch("sift_py.ingestion._internal.ingest.RuleService"):
|
|
221
|
+
svc = _IngestionServiceImpl(
|
|
222
|
+
channel=transport_channel,
|
|
223
|
+
config=telemetry_config,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Non-existent flow
|
|
227
|
+
with pytest.raises(IngestionValidationError, match="could not be found"):
|
|
228
|
+
svc.try_create_ingestion_request(
|
|
229
|
+
flow_name="lerg", # typo
|
|
230
|
+
timestamp=datetime.now(timezone.utc),
|
|
231
|
+
channel_values=[
|
|
232
|
+
{"channel_name": "logs", "value": string_value("foobar")},
|
|
233
|
+
],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Duplicate values for channel
|
|
237
|
+
with pytest.raises(IngestionValidationError, match="multiple values"):
|
|
238
|
+
svc.try_create_ingestion_request(
|
|
239
|
+
flow_name="log",
|
|
240
|
+
timestamp=datetime.now(timezone.utc),
|
|
241
|
+
channel_values=[
|
|
242
|
+
{"channel_name": "logs", "value": string_value("foobar")},
|
|
243
|
+
{"channel_name": "logs", "value": string_value("foobar")},
|
|
244
|
+
],
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Wrong channel value type
|
|
248
|
+
with pytest.raises(IngestionValidationError, match="Expected value"):
|
|
249
|
+
svc.try_create_ingestion_request(
|
|
250
|
+
flow_name="log",
|
|
251
|
+
timestamp=datetime.now(timezone.utc),
|
|
252
|
+
channel_values=[
|
|
253
|
+
{"channel_name": "logs", "value": int32_value(32)},
|
|
254
|
+
],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Wrong channel for flow
|
|
258
|
+
with pytest.raises(IngestionValidationError, match="Unexpected channel"):
|
|
259
|
+
svc.try_create_ingestion_request(
|
|
260
|
+
flow_name="log",
|
|
261
|
+
timestamp=datetime.now(timezone.utc),
|
|
262
|
+
channel_values=[
|
|
263
|
+
{"channel_name": "voltage", "value": double_value(32)},
|
|
264
|
+
],
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_ingestion_service_init_with_rules(mocker: MockFixture):
|
|
269
|
+
"""
|
|
270
|
+
Ensures that rules are created and updated to include the asset from the
|
|
271
|
+
telemetry config when the ingestion service is initialized.
|
|
272
|
+
"""
|
|
273
|
+
voltage_channel = ChannelConfig(
|
|
274
|
+
name="voltage",
|
|
275
|
+
component="motor",
|
|
276
|
+
data_type=ChannelDataType.DOUBLE,
|
|
277
|
+
)
|
|
278
|
+
pressure_channel = ChannelConfig(
|
|
279
|
+
name="pressure",
|
|
280
|
+
data_type=ChannelDataType.INT_64,
|
|
281
|
+
)
|
|
282
|
+
logs_channel = ChannelConfig(
|
|
283
|
+
name="logs",
|
|
284
|
+
data_type=ChannelDataType.STRING,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
rule_on_voltage = RuleConfig(
|
|
288
|
+
name="voltage_rule",
|
|
289
|
+
description="",
|
|
290
|
+
expression="$1 > 10",
|
|
291
|
+
channel_references=[
|
|
292
|
+
{"channel_reference": "$1", "channel_identifier": voltage_channel.fqn()},
|
|
293
|
+
],
|
|
294
|
+
action=RuleActionCreateDataReviewAnnotation(
|
|
295
|
+
assignee="bob@example.com",
|
|
296
|
+
tags=["motor"],
|
|
297
|
+
),
|
|
298
|
+
rule_client_key="voltage-rule-key",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
rule_on_pressure = RuleConfig(
|
|
302
|
+
name="pressure_rule",
|
|
303
|
+
description="",
|
|
304
|
+
expression="$1 > 10",
|
|
305
|
+
channel_references=[
|
|
306
|
+
{"channel_reference": "$1", "channel_identifier": pressure_channel.fqn()},
|
|
307
|
+
],
|
|
308
|
+
action=RuleActionCreateDataReviewAnnotation(
|
|
309
|
+
assignee="bob@example.com",
|
|
310
|
+
tags=["barometer"],
|
|
311
|
+
),
|
|
312
|
+
rule_client_key="pressure-rule-key",
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
mock_ingestion_config = IngestionConfigPb(
|
|
316
|
+
ingestion_config_id="my-ingestion-config",
|
|
317
|
+
asset_id="my-asset-id",
|
|
318
|
+
client_key="my-client-key",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
mock_get_or_create_ingestion_config = mocker.patch.object(
|
|
322
|
+
_IngestionServiceImpl, "_get_or_create_ingestion_config"
|
|
323
|
+
)
|
|
324
|
+
mock_get_or_create_ingestion_config.return_value = mock_ingestion_config
|
|
325
|
+
|
|
326
|
+
mock_update_flow_configs = mocker.patch.object(_IngestionServiceImpl, "_update_flow_configs")
|
|
327
|
+
mock_update_flow_configs.return_value = None
|
|
328
|
+
|
|
329
|
+
telemetry_config = TelemetryConfig(
|
|
330
|
+
asset_name="my-asset",
|
|
331
|
+
ingestion_client_key=mock_ingestion_config.client_key,
|
|
332
|
+
flows=[
|
|
333
|
+
FlowConfig(
|
|
334
|
+
name="reading",
|
|
335
|
+
channels=[voltage_channel, pressure_channel],
|
|
336
|
+
),
|
|
337
|
+
FlowConfig(
|
|
338
|
+
name="pressure",
|
|
339
|
+
channels=[pressure_channel],
|
|
340
|
+
),
|
|
341
|
+
FlowConfig(
|
|
342
|
+
name="log",
|
|
343
|
+
channels=[logs_channel],
|
|
344
|
+
),
|
|
345
|
+
],
|
|
346
|
+
rules=[rule_on_voltage, rule_on_pressure],
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
mock_channel = MockChannel()
|
|
350
|
+
|
|
351
|
+
with mocker.patch("sift_py.ingestion._internal.ingest.RuleService"):
|
|
352
|
+
svc = _IngestionServiceImpl(
|
|
353
|
+
channel=mock_channel,
|
|
354
|
+
config=telemetry_config,
|
|
355
|
+
)
|
|
356
|
+
for rule in svc.rules:
|
|
357
|
+
assert rule.asset_names == ["my-asset"]
|