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/rule/service.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union, cast
|
|
6
|
+
|
|
7
|
+
from sift.annotations.v1.annotations_pb2 import AnnotationType
|
|
8
|
+
from sift.assets.v1.assets_pb2 import Asset, ListAssetsRequest, ListAssetsResponse
|
|
9
|
+
from sift.assets.v1.assets_pb2_grpc import AssetServiceStub
|
|
10
|
+
from sift.channels.v2.channels_pb2_grpc import ChannelServiceStub
|
|
11
|
+
from sift.rules.v1.rules_pb2 import (
|
|
12
|
+
ANNOTATION,
|
|
13
|
+
AnnotationActionConfiguration,
|
|
14
|
+
CalculatedChannelConfig,
|
|
15
|
+
CreateRuleRequest,
|
|
16
|
+
GetRuleRequest,
|
|
17
|
+
GetRuleResponse,
|
|
18
|
+
Rule,
|
|
19
|
+
RuleActionConfiguration,
|
|
20
|
+
RuleAssetConfiguration,
|
|
21
|
+
RuleConditionExpression,
|
|
22
|
+
UpdateActionRequest,
|
|
23
|
+
UpdateConditionRequest,
|
|
24
|
+
UpdateRuleRequest,
|
|
25
|
+
)
|
|
26
|
+
from sift.rules.v1.rules_pb2_grpc import RuleServiceStub
|
|
27
|
+
from sift.users.v2.users_pb2_grpc import UserServiceStub
|
|
28
|
+
|
|
29
|
+
from sift_py._internal.cel import cel_in
|
|
30
|
+
from sift_py._internal.channel import get_channels
|
|
31
|
+
from sift_py._internal.user import get_active_users
|
|
32
|
+
from sift_py.grpc.transport import SiftChannel
|
|
33
|
+
from sift_py.ingestion._internal.channel import channel_reference_from_fqn
|
|
34
|
+
from sift_py.ingestion.channel import channel_fqn
|
|
35
|
+
from sift_py.rule.config import (
|
|
36
|
+
ExpressionChannelReference,
|
|
37
|
+
ExpressionChannelReferenceChannelConfig,
|
|
38
|
+
RuleAction,
|
|
39
|
+
RuleActionAnnotationKind,
|
|
40
|
+
RuleActionCreateDataReviewAnnotation,
|
|
41
|
+
RuleActionCreatePhaseAnnotation,
|
|
42
|
+
RuleActionKind,
|
|
43
|
+
RuleConfig,
|
|
44
|
+
)
|
|
45
|
+
from sift_py.yaml.rule import load_rule_modules
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RuleService:
|
|
49
|
+
"""
|
|
50
|
+
A service for managing rules. Allows for loading rules from YAML and creating or updating them in the Sift API.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
_asset_service_stub: AssetServiceStub
|
|
54
|
+
_channel_service_stub: ChannelServiceStub
|
|
55
|
+
_rule_service_stub: RuleServiceStub
|
|
56
|
+
_user_service_stub: UserServiceStub
|
|
57
|
+
|
|
58
|
+
def __init__(self, channel: SiftChannel):
|
|
59
|
+
self._asset_service_stub = AssetServiceStub(channel)
|
|
60
|
+
self._channel_service_stub = ChannelServiceStub(channel)
|
|
61
|
+
self._rule_service_stub = RuleServiceStub(channel)
|
|
62
|
+
self._user_service_stub = UserServiceStub(channel)
|
|
63
|
+
|
|
64
|
+
def load_rules_from_yaml(
|
|
65
|
+
self,
|
|
66
|
+
paths: List[Path],
|
|
67
|
+
named_expressions: Optional[Dict[str, str]] = None,
|
|
68
|
+
) -> List[RuleConfig]:
|
|
69
|
+
"""
|
|
70
|
+
Loads rules from a YAML spec, and creates or updates the rules in the Sift API.
|
|
71
|
+
For more on rule YAML definitions, see `sift_py.ingestion.config.yaml.spec.RuleYamlSpec`.
|
|
72
|
+
"""
|
|
73
|
+
module_rules = load_rule_modules(paths)
|
|
74
|
+
|
|
75
|
+
rule_configs = []
|
|
76
|
+
for rule_yaml in module_rules:
|
|
77
|
+
rule_name = rule_yaml["name"]
|
|
78
|
+
|
|
79
|
+
# First parse channel references
|
|
80
|
+
yaml_channel_references = rule_yaml.get("channel_references", [])
|
|
81
|
+
|
|
82
|
+
rule_channel_references: List[
|
|
83
|
+
Union[ExpressionChannelReference, ExpressionChannelReferenceChannelConfig]
|
|
84
|
+
] = []
|
|
85
|
+
|
|
86
|
+
for channel_ref in yaml_channel_references:
|
|
87
|
+
for ref, channel_config in channel_ref.items():
|
|
88
|
+
if isinstance(channel_config, dict):
|
|
89
|
+
name = channel_config.get("name", "")
|
|
90
|
+
component = channel_config.get("component", "")
|
|
91
|
+
elif isinstance(channel_config, str):
|
|
92
|
+
channel_reference = channel_reference_from_fqn(channel_config)
|
|
93
|
+
name = channel_reference.name
|
|
94
|
+
component = channel_reference.component
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Channel reference '{channel_config}' must be a string or a ChannelConfigYamlSpec"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
rule_channel_references.append(
|
|
101
|
+
{
|
|
102
|
+
"channel_reference": ref,
|
|
103
|
+
"channel_identifier": channel_fqn(
|
|
104
|
+
{
|
|
105
|
+
"channel_name": name,
|
|
106
|
+
"component": component,
|
|
107
|
+
}
|
|
108
|
+
),
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if not rule_channel_references:
|
|
113
|
+
raise ValueError(f"Rule of name '{rule_yaml['name']}' requires channel_references")
|
|
114
|
+
|
|
115
|
+
# Parse expression for named expressions and sub expressions
|
|
116
|
+
expression = rule_yaml["expression"]
|
|
117
|
+
if isinstance(expression, dict):
|
|
118
|
+
expression_name = expression.get("name", "")
|
|
119
|
+
if not named_expressions:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"Rule '{rule_name}' requires named expressions, but none were provided."
|
|
122
|
+
)
|
|
123
|
+
expression = named_expressions.get(expression_name, "")
|
|
124
|
+
if not expression:
|
|
125
|
+
raise ValueError(f"Named expression '{expression_name}' could not be found.")
|
|
126
|
+
|
|
127
|
+
yaml_subexprs = rule_yaml.get("sub_expressions", [])
|
|
128
|
+
subexpr: Dict[str, Any] = {}
|
|
129
|
+
for sub in yaml_subexprs:
|
|
130
|
+
for iden, value in sub.items():
|
|
131
|
+
subexpr[iden] = value
|
|
132
|
+
|
|
133
|
+
# Create rule actions
|
|
134
|
+
tags = rule_yaml.get("tags", [])
|
|
135
|
+
annotation_type = RuleActionAnnotationKind.from_str(rule_yaml["type"])
|
|
136
|
+
action: RuleAction = RuleActionCreatePhaseAnnotation(tags)
|
|
137
|
+
if annotation_type == RuleActionAnnotationKind.REVIEW:
|
|
138
|
+
action = RuleActionCreateDataReviewAnnotation(
|
|
139
|
+
assignee=rule_yaml.get("assignee"),
|
|
140
|
+
tags=tags,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Append rule config to list
|
|
144
|
+
rule_configs.append(
|
|
145
|
+
RuleConfig(
|
|
146
|
+
name=rule_name,
|
|
147
|
+
rule_client_key=rule_yaml.get("rule_client_key"),
|
|
148
|
+
description=rule_yaml.get("description", ""),
|
|
149
|
+
expression=str(expression),
|
|
150
|
+
action=action,
|
|
151
|
+
channel_references=rule_channel_references,
|
|
152
|
+
asset_names=rule_yaml.get("asset_names", []),
|
|
153
|
+
sub_expressions=subexpr,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Create all the rules
|
|
158
|
+
for rule_config in rule_configs:
|
|
159
|
+
self.create_or_update_rule(rule_config)
|
|
160
|
+
|
|
161
|
+
return rule_configs
|
|
162
|
+
|
|
163
|
+
def create_or_update_rules(self, rule_configs: List[RuleConfig]):
|
|
164
|
+
"""
|
|
165
|
+
Create or update a list of rules via a list of RuleConfigs.
|
|
166
|
+
See `sift_py.rule.config.RuleConfig` for more information on configuation parameters for rules.
|
|
167
|
+
"""
|
|
168
|
+
for config in rule_configs:
|
|
169
|
+
self.create_or_update_rule(config)
|
|
170
|
+
|
|
171
|
+
def attach_asset(self, rule: Union[str, RuleConfig], asset_names: List[str]) -> RuleConfig:
|
|
172
|
+
"""
|
|
173
|
+
Associates a rule with an asset by name. The asset must already exist in the Sift API.
|
|
174
|
+
The provided rule may either be a rule client key, rule id, or a RuleConfig.
|
|
175
|
+
"""
|
|
176
|
+
return self._attach_or_detach_asset(rule, asset_names, attach=True)
|
|
177
|
+
|
|
178
|
+
def detach_asset(self, rule: Union[str, RuleConfig], asset_names: List[str]) -> RuleConfig:
|
|
179
|
+
"""
|
|
180
|
+
Disassociates a rule from an asset by name. The asset must already exist in the Sift API.
|
|
181
|
+
The provided rule may either be a rule client key, rule id, or a RuleConfig.
|
|
182
|
+
"""
|
|
183
|
+
return self._attach_or_detach_asset(rule, asset_names, attach=False)
|
|
184
|
+
|
|
185
|
+
def _attach_or_detach_asset(
|
|
186
|
+
self, rule: Union[str, RuleConfig], asset_names: List[str], attach: bool
|
|
187
|
+
) -> RuleConfig:
|
|
188
|
+
assets = self._get_assets(names=asset_names)
|
|
189
|
+
if not assets:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"Cannot find all assets in list '{asset_names}'. One of these assets does not exist."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if isinstance(rule, str):
|
|
195
|
+
rule = cast(RuleConfig, self.get_rule(rule))
|
|
196
|
+
|
|
197
|
+
if attach:
|
|
198
|
+
if not rule.asset_names:
|
|
199
|
+
rule.asset_names = asset_names
|
|
200
|
+
else:
|
|
201
|
+
rule.asset_names.extend(asset_names)
|
|
202
|
+
else:
|
|
203
|
+
rule.asset_names = list(set(rule.asset_names) ^ set(asset_names))
|
|
204
|
+
|
|
205
|
+
if not rule.asset_names:
|
|
206
|
+
raise ValueError(f"Rule '{rule.name}' must be associated with at least one asset.")
|
|
207
|
+
|
|
208
|
+
req = self._update_req_from_rule_config(rule)
|
|
209
|
+
self._rule_service_stub.UpdateRule(req)
|
|
210
|
+
|
|
211
|
+
return rule
|
|
212
|
+
|
|
213
|
+
def create_or_update_rule(self, config: RuleConfig):
|
|
214
|
+
"""
|
|
215
|
+
Create or update a rule via a RuleConfig. The config must contain a rule_client_key or an exception will be raised.
|
|
216
|
+
If a rule with the given client key already exists it will be updated, otherwise it will be created.
|
|
217
|
+
See `sift_py.rule.config.RuleConfig` for more information on configuation parameters for rules.
|
|
218
|
+
"""
|
|
219
|
+
if not config.rule_client_key:
|
|
220
|
+
raise ValueError(f"Rule of name '{config.name}' requires a rule_client_key")
|
|
221
|
+
|
|
222
|
+
rule = self._get_rule_from_client_key(config.rule_client_key)
|
|
223
|
+
if rule:
|
|
224
|
+
self._update_rule(config, rule)
|
|
225
|
+
else:
|
|
226
|
+
self._create_rule(config)
|
|
227
|
+
|
|
228
|
+
def _update_rule(self, updated_config: RuleConfig, rule: Rule):
|
|
229
|
+
req = self._update_req_from_rule_config(updated_config, rule)
|
|
230
|
+
self._rule_service_stub.UpdateRule(req)
|
|
231
|
+
|
|
232
|
+
def _create_rule(self, config: RuleConfig):
|
|
233
|
+
req = self._update_req_from_rule_config(config)
|
|
234
|
+
self._rule_service_stub.CreateRule(CreateRuleRequest(update=req))
|
|
235
|
+
|
|
236
|
+
def _update_req_from_rule(self, rule: Rule) -> UpdateRuleRequest:
|
|
237
|
+
return UpdateRuleRequest(
|
|
238
|
+
organization_id=rule.organization_id,
|
|
239
|
+
rule_id=rule.rule_id,
|
|
240
|
+
client_key=rule.client_key,
|
|
241
|
+
name=rule.name,
|
|
242
|
+
description=rule.description,
|
|
243
|
+
conditions=[
|
|
244
|
+
UpdateConditionRequest(
|
|
245
|
+
rule_condition_id=condition.rule_condition_id,
|
|
246
|
+
actions=[
|
|
247
|
+
UpdateActionRequest(
|
|
248
|
+
rule_action_id=action.rule_action_id,
|
|
249
|
+
action_type=action.action_type,
|
|
250
|
+
configuration=action.configuration,
|
|
251
|
+
)
|
|
252
|
+
for action in condition.actions
|
|
253
|
+
],
|
|
254
|
+
expression=condition.expression,
|
|
255
|
+
)
|
|
256
|
+
for condition in rule.conditions
|
|
257
|
+
],
|
|
258
|
+
asset_configuration=RuleAssetConfiguration(
|
|
259
|
+
asset_ids=rule.asset_configuration.asset_ids,
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def _update_req_from_rule_config(
|
|
264
|
+
self, config: RuleConfig, rule: Optional[Rule] = None
|
|
265
|
+
) -> UpdateRuleRequest:
|
|
266
|
+
if not config.expression:
|
|
267
|
+
raise ValueError(
|
|
268
|
+
"Cannot create a rule with an empty expression."
|
|
269
|
+
"See `sift_py.rule.config.RuleConfig` for more information on rule configuration."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if not config.action:
|
|
273
|
+
raise ValueError(
|
|
274
|
+
"Cannot create a rule with no corresponding action."
|
|
275
|
+
"See `sift_py.rule.config.RuleAction` for available actions."
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# TODO:
|
|
279
|
+
# - once we have TagService_ListTags we can do asset-agnostic rules via tags
|
|
280
|
+
assets = self._get_assets(names=config.asset_names) if config.asset_names else None
|
|
281
|
+
|
|
282
|
+
actions = []
|
|
283
|
+
if config.action.kind() == RuleActionKind.NOTIFICATION:
|
|
284
|
+
raise NotImplementedError(
|
|
285
|
+
"Notification actions are not yet supported."
|
|
286
|
+
"Please contact the Sift team for assistance."
|
|
287
|
+
)
|
|
288
|
+
elif config.action.kind() == RuleActionKind.ANNOTATION:
|
|
289
|
+
if isinstance(config.action, RuleActionCreateDataReviewAnnotation):
|
|
290
|
+
assignee = config.action.assignee
|
|
291
|
+
user_id = None
|
|
292
|
+
if assignee:
|
|
293
|
+
users = get_active_users(
|
|
294
|
+
user_service=self._user_service_stub,
|
|
295
|
+
filter=f"name=='{assignee}'",
|
|
296
|
+
)
|
|
297
|
+
if not users:
|
|
298
|
+
raise ValueError(f"Cannot find user '{assignee}'.")
|
|
299
|
+
if len(users) > 1:
|
|
300
|
+
raise ValueError(f"Multiple users found with name '{assignee}'.")
|
|
301
|
+
user_id = users[0].user_id
|
|
302
|
+
|
|
303
|
+
action_config = UpdateActionRequest(
|
|
304
|
+
action_type=ANNOTATION,
|
|
305
|
+
configuration=RuleActionConfiguration(
|
|
306
|
+
annotation=AnnotationActionConfiguration(
|
|
307
|
+
assigned_to_user_id=user_id,
|
|
308
|
+
# tag_ids=config.action.tags, # TODO: Requires TagService
|
|
309
|
+
)
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
actions.append(action_config)
|
|
313
|
+
elif isinstance(config.action, RuleActionCreatePhaseAnnotation):
|
|
314
|
+
action_config = UpdateActionRequest(
|
|
315
|
+
action_type=ANNOTATION,
|
|
316
|
+
configuration=RuleActionConfiguration(
|
|
317
|
+
annotation=AnnotationActionConfiguration(
|
|
318
|
+
# tag_ids=config.action.tags, # TODO: Requires TagService
|
|
319
|
+
)
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
channel_references = {}
|
|
324
|
+
for channel_reference in config.channel_references:
|
|
325
|
+
ref = channel_reference["channel_reference"]
|
|
326
|
+
ident = channel_reference_from_fqn(channel_reference["channel_identifier"])
|
|
327
|
+
channel_references[ref] = ident
|
|
328
|
+
|
|
329
|
+
if assets and channel_references:
|
|
330
|
+
identifiers = [ident.name for ident in channel_references.values()]
|
|
331
|
+
components = [ident.component for ident in channel_references.values()]
|
|
332
|
+
|
|
333
|
+
# Create CEL search filters
|
|
334
|
+
name_in = cel_in("name", identifiers)
|
|
335
|
+
component_in = cel_in("component", components)
|
|
336
|
+
|
|
337
|
+
# Validate channels are present within each asset
|
|
338
|
+
for asset in assets:
|
|
339
|
+
found_channels = get_channels(
|
|
340
|
+
channel_service=self._channel_service_stub,
|
|
341
|
+
filter=f"asset_id == '{asset.asset_id}' && {name_in} && {component_in}",
|
|
342
|
+
)
|
|
343
|
+
found_channels_names = [channel.name for channel in found_channels]
|
|
344
|
+
|
|
345
|
+
missing_channels = set(identifiers) ^ set(found_channels_names)
|
|
346
|
+
if missing_channels:
|
|
347
|
+
raise RuntimeError(
|
|
348
|
+
f"Asset {asset.name} is missing channels required for rule {config.name}: {missing_channels}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
rule_id = None
|
|
352
|
+
organization_id = ""
|
|
353
|
+
if rule:
|
|
354
|
+
rule_id = rule.rule_id
|
|
355
|
+
organization_id = rule.organization_id
|
|
356
|
+
|
|
357
|
+
return UpdateRuleRequest(
|
|
358
|
+
organization_id=organization_id,
|
|
359
|
+
rule_id=rule_id,
|
|
360
|
+
client_key=config.rule_client_key,
|
|
361
|
+
name=config.name,
|
|
362
|
+
description=config.description,
|
|
363
|
+
conditions=[
|
|
364
|
+
UpdateConditionRequest(
|
|
365
|
+
actions=actions,
|
|
366
|
+
expression=RuleConditionExpression(
|
|
367
|
+
calculated_channel=CalculatedChannelConfig(
|
|
368
|
+
expression=config.expression,
|
|
369
|
+
channel_references=channel_references,
|
|
370
|
+
)
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
],
|
|
374
|
+
asset_configuration=RuleAssetConfiguration(
|
|
375
|
+
asset_ids=[asset.asset_id for asset in assets] if assets else None,
|
|
376
|
+
),
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def get_rule(self, rule: str) -> Optional[RuleConfig]:
|
|
380
|
+
"""
|
|
381
|
+
Get a rule by rule id or client key. Returns a RuleConfig if the rule exists, otherwise None.
|
|
382
|
+
"""
|
|
383
|
+
rule_pb = self._get_rule_from_client_key(rule) or self._get_rule_from_rule_id(rule)
|
|
384
|
+
if not rule_pb:
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
channel_references: List[ExpressionChannelReference] = []
|
|
388
|
+
expression = ""
|
|
389
|
+
action: Optional[
|
|
390
|
+
Union[RuleActionCreateDataReviewAnnotation, RuleActionCreatePhaseAnnotation]
|
|
391
|
+
] = None
|
|
392
|
+
for condition in rule_pb.conditions:
|
|
393
|
+
expression = condition.expression.calculated_channel.expression
|
|
394
|
+
for ref, id in condition.expression.calculated_channel.channel_references.items():
|
|
395
|
+
channel_references.append(
|
|
396
|
+
{
|
|
397
|
+
"channel_reference": ref,
|
|
398
|
+
"channel_identifier": id.name,
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
for action_config in condition.actions:
|
|
402
|
+
annotation_type = action_config.configuration.annotation.annotation_type
|
|
403
|
+
if annotation_type == AnnotationType.ANNOTATION_TYPE_PHASE:
|
|
404
|
+
action = RuleActionCreatePhaseAnnotation(
|
|
405
|
+
tags=[tag for tag in action_config.configuration.annotation.tag_ids],
|
|
406
|
+
)
|
|
407
|
+
else:
|
|
408
|
+
assignee = action_config.configuration.annotation.assigned_to_user_id
|
|
409
|
+
action = RuleActionCreateDataReviewAnnotation(
|
|
410
|
+
assignee=assignee,
|
|
411
|
+
tags=[tag for tag in action_config.configuration.annotation.tag_ids],
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
assets = self._get_assets(
|
|
415
|
+
ids=[asset_id for asset_id in rule_pb.asset_configuration.asset_ids]
|
|
416
|
+
)
|
|
417
|
+
asset_names = [asset.name for asset in assets]
|
|
418
|
+
|
|
419
|
+
rule_config = RuleConfig(
|
|
420
|
+
name=rule_pb.name,
|
|
421
|
+
description=rule_pb.description,
|
|
422
|
+
rule_client_key=rule_pb.client_key,
|
|
423
|
+
channel_references=channel_references, # type: ignore
|
|
424
|
+
asset_names=asset_names,
|
|
425
|
+
action=action,
|
|
426
|
+
expression=expression,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return rule_config
|
|
430
|
+
|
|
431
|
+
def _get_rule_from_client_key(self, client_key: str) -> Optional[Rule]:
|
|
432
|
+
req = GetRuleRequest(client_key=client_key)
|
|
433
|
+
try:
|
|
434
|
+
res = cast(GetRuleResponse, self._rule_service_stub.GetRule(req))
|
|
435
|
+
return res.rule or None
|
|
436
|
+
except:
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
def _get_rule_from_rule_id(self, rule_id: str) -> Optional[Rule]:
|
|
440
|
+
req = GetRuleRequest(rule_id=rule_id)
|
|
441
|
+
try:
|
|
442
|
+
res = cast(GetRuleResponse, self._rule_service_stub.GetRule(req))
|
|
443
|
+
return res.rule or None
|
|
444
|
+
except:
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
def _get_assets(self, names: List[str] = [], ids: List[str] = []) -> List[Asset]:
|
|
448
|
+
def get_assets_with_filter(cel_filter: str):
|
|
449
|
+
assets: List[Asset] = []
|
|
450
|
+
next_page_token = ""
|
|
451
|
+
while True:
|
|
452
|
+
req = ListAssetsRequest(
|
|
453
|
+
filter=cel_filter,
|
|
454
|
+
page_size=1_000,
|
|
455
|
+
page_token=next_page_token,
|
|
456
|
+
)
|
|
457
|
+
res = cast(ListAssetsResponse, self._asset_service_stub.ListAssets(req))
|
|
458
|
+
assets.extend(res.assets)
|
|
459
|
+
|
|
460
|
+
if not res.next_page_token:
|
|
461
|
+
break
|
|
462
|
+
next_page_token = res.next_page_token
|
|
463
|
+
|
|
464
|
+
return assets
|
|
465
|
+
|
|
466
|
+
if names:
|
|
467
|
+
names_cel = cel_in("name", names)
|
|
468
|
+
return get_assets_with_filter(names_cel)
|
|
469
|
+
elif ids:
|
|
470
|
+
ids_cel = cel_in("asset_id", ids)
|
|
471
|
+
return get_assets_with_filter(ids_cel)
|
|
472
|
+
else:
|
|
473
|
+
return []
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@dataclass
|
|
477
|
+
class RuleChannelReference:
|
|
478
|
+
"""
|
|
479
|
+
Convenient wrapper to map rule names to relevant channel references
|
|
480
|
+
when creating rules from yaml.
|
|
481
|
+
"""
|
|
482
|
+
|
|
483
|
+
rule_name: str
|
|
484
|
+
channel_references: Dict[str, Any]
|
sift_py/yaml/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from sift_py.ingestion.config.yaml.error import YamlConfigError
|
|
4
|
+
from sift_py.yaml import channel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test__validate_channel_anchor():
|
|
8
|
+
channel._validate_channel_anchor("foo")
|
|
9
|
+
|
|
10
|
+
with pytest.raises(YamlConfigError, match="Expected '<str>'"):
|
|
11
|
+
channel._validate_channel_anchor(3)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test__validate_enum_type():
|
|
15
|
+
channel._validate_enum_type(
|
|
16
|
+
{
|
|
17
|
+
"name": "foo",
|
|
18
|
+
"key": 0,
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
with pytest.raises(YamlConfigError, match="Expected 'key' to be <int> but it is <str>"):
|
|
23
|
+
channel._validate_enum_type(
|
|
24
|
+
{
|
|
25
|
+
"name": "foo",
|
|
26
|
+
"key": "foobar",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
with pytest.raises(YamlConfigError, match="Expected 'name' to be <str> but it is <int>"):
|
|
31
|
+
channel._validate_enum_type(
|
|
32
|
+
{
|
|
33
|
+
"name": 123,
|
|
34
|
+
"key": 0,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test__validate_bit_field_element():
|
|
40
|
+
channel._validate_bit_field_element(
|
|
41
|
+
{
|
|
42
|
+
"name": "heater",
|
|
43
|
+
"index": 0,
|
|
44
|
+
"bit_count": 3,
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
with pytest.raises(YamlConfigError, match="Expected 'name' to be <str> but it is <int>"):
|
|
49
|
+
channel._validate_bit_field_element(
|
|
50
|
+
{
|
|
51
|
+
"name": 0,
|
|
52
|
+
"index": 0,
|
|
53
|
+
"bit_count": 3,
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
with pytest.raises(YamlConfigError, match="Expected 'index' to be <int> but it is <str>"):
|
|
58
|
+
channel._validate_bit_field_element(
|
|
59
|
+
{
|
|
60
|
+
"name": "heater",
|
|
61
|
+
"index": "foobar",
|
|
62
|
+
"bit_count": 3,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
with pytest.raises(YamlConfigError, match="Expected 'bit_count' to be <int> but it is <str>"):
|
|
67
|
+
channel._validate_bit_field_element(
|
|
68
|
+
{
|
|
69
|
+
"name": "heater",
|
|
70
|
+
"index": 0,
|
|
71
|
+
"bit_count": "foo",
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test__validate_channel():
|
|
77
|
+
channel._validate_channel(
|
|
78
|
+
{
|
|
79
|
+
"name": "force",
|
|
80
|
+
"data_type": "double",
|
|
81
|
+
"unit": "N",
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
with pytest.raises(YamlConfigError, match="Expected 'data_type' to be"):
|
|
86
|
+
channel._validate_channel(
|
|
87
|
+
{
|
|
88
|
+
"name": "force",
|
|
89
|
+
"data_type": "dubble",
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
with pytest.raises(YamlConfigError, match="Expected 'description' to be <str> but it is <int>"):
|
|
94
|
+
channel._validate_channel(
|
|
95
|
+
{
|
|
96
|
+
"name": "force",
|
|
97
|
+
"description": 9001,
|
|
98
|
+
"data_type": "double",
|
|
99
|
+
"unit": "N",
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
with pytest.raises(YamlConfigError, match="Expected 'unit' to be <str> but it is <list>"):
|
|
104
|
+
channel._validate_channel(
|
|
105
|
+
{
|
|
106
|
+
"name": "force",
|
|
107
|
+
"description": "use the force",
|
|
108
|
+
"data_type": "double",
|
|
109
|
+
"component": "motor",
|
|
110
|
+
"unit": [1, 2, 3],
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
with pytest.raises(YamlConfigError, match="Expected 'component' to be <str> but it is <dict>"):
|
|
115
|
+
channel._validate_channel(
|
|
116
|
+
{
|
|
117
|
+
"name": "force",
|
|
118
|
+
"description": "use the force",
|
|
119
|
+
"data_type": "double",
|
|
120
|
+
"component": {},
|
|
121
|
+
"unit": "N",
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
with pytest.raises(YamlConfigError, match="should not have 'bit_field_elements' set"):
|
|
126
|
+
channel._validate_channel(
|
|
127
|
+
{
|
|
128
|
+
"name": "force",
|
|
129
|
+
"description": "use the force",
|
|
130
|
+
"data_type": "double",
|
|
131
|
+
"component": "motor",
|
|
132
|
+
"unit": "N",
|
|
133
|
+
"bit_field_elements": [{"name": "heat", "index": 0, "bit_count": 3}],
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
channel._validate_channel(
|
|
138
|
+
{
|
|
139
|
+
"name": "force",
|
|
140
|
+
"description": "use the force",
|
|
141
|
+
"data_type": "bit_field",
|
|
142
|
+
"component": "motor",
|
|
143
|
+
"unit": "N",
|
|
144
|
+
"bit_field_elements": [{"name": "heat", "index": 0, "bit_count": 3}],
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
with pytest.raises(YamlConfigError, match="should not have 'enum_types' set"):
|
|
149
|
+
channel._validate_channel(
|
|
150
|
+
{
|
|
151
|
+
"name": "force",
|
|
152
|
+
"description": "use the force",
|
|
153
|
+
"data_type": "double",
|
|
154
|
+
"component": "motor",
|
|
155
|
+
"unit": "N",
|
|
156
|
+
"enum_types": [{"name": "heat", "key": 0}],
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
channel._validate_channel(
|
|
161
|
+
{
|
|
162
|
+
"name": "force",
|
|
163
|
+
"description": "use the force",
|
|
164
|
+
"data_type": "enum",
|
|
165
|
+
"component": "motor",
|
|
166
|
+
"unit": "N",
|
|
167
|
+
"enum_types": [{"name": "heat", "key": 0}],
|
|
168
|
+
}
|
|
169
|
+
)
|