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,207 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from sift_py.ingestion.config.yaml.error import YamlConfigError
|
|
4
|
+
from sift_py.yaml import rule
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test__validate_sub_expression():
|
|
8
|
+
rule._validate_sub_expression({"$mass": 10})
|
|
9
|
+
rule._validate_sub_expression({"$m": 10})
|
|
10
|
+
|
|
11
|
+
with pytest.raises(YamlConfigError, match="Invalid sub-expression key"):
|
|
12
|
+
rule._validate_sub_expression({"mass": 10})
|
|
13
|
+
|
|
14
|
+
with pytest.raises(YamlConfigError, match="Invalid sub-expression key"):
|
|
15
|
+
rule._validate_sub_expression({"$!mass": 10})
|
|
16
|
+
|
|
17
|
+
with pytest.raises(YamlConfigError, match="Invalid sub-expression key"):
|
|
18
|
+
rule._validate_sub_expression({"$$mass": 10})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test__validate_rule():
|
|
22
|
+
rule._validate_rule(
|
|
23
|
+
{
|
|
24
|
+
"name": "overheat_rule",
|
|
25
|
+
"description": "some_description",
|
|
26
|
+
"expression": "$1 > 10 && $2 > 10",
|
|
27
|
+
"type": "review",
|
|
28
|
+
"assignee": "homer@example.com",
|
|
29
|
+
"tags": ["foo", "bar"],
|
|
30
|
+
"channel_references": [
|
|
31
|
+
{"$1": {"name": "voltage", "data_type": "double"}},
|
|
32
|
+
{"$2": {"name": "vehicle_state", "data_type": "double"}},
|
|
33
|
+
],
|
|
34
|
+
"rule_client_key": "overheat_rule_key",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Rule with tag and asset names
|
|
39
|
+
rule._validate_rule(
|
|
40
|
+
{
|
|
41
|
+
"name": "overheat_rule",
|
|
42
|
+
"description": "some_description",
|
|
43
|
+
"expression": "$1 > 10 && $2 > 10",
|
|
44
|
+
"type": "review",
|
|
45
|
+
"assignee": "homer@example.com",
|
|
46
|
+
"tags": ["foo", "bar"],
|
|
47
|
+
"channel_references": [
|
|
48
|
+
{"$1": {"name": "voltage", "data_type": "double"}},
|
|
49
|
+
{"$2": {"name": "vehicle_state", "data_type": "double"}},
|
|
50
|
+
],
|
|
51
|
+
"asset_names": ["NostromoLV426"],
|
|
52
|
+
"tag_names": ["vehicle"],
|
|
53
|
+
"rule_client_key": "overheat_rule_key",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
with pytest.raises(YamlConfigError, match="Expected 'name' to be <str> but it is <int>"):
|
|
58
|
+
rule._validate_rule(
|
|
59
|
+
{
|
|
60
|
+
"name": 0,
|
|
61
|
+
"description": "some_description",
|
|
62
|
+
"expression": "$1 > 10 && $2 > 10",
|
|
63
|
+
"type": "review",
|
|
64
|
+
"assignee": "homer@example.com",
|
|
65
|
+
"tags": ["foo", "bar"],
|
|
66
|
+
"channel_references": [
|
|
67
|
+
{"$1": {"name": "voltage", "data_type": "double"}},
|
|
68
|
+
{"$2": {"name": "vehicle_state", "data_type": "double"}},
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with pytest.raises(YamlConfigError, match="Expected 'description' to be <str> but it is <int>"):
|
|
74
|
+
rule._validate_rule(
|
|
75
|
+
{
|
|
76
|
+
"name": "overheat_rule",
|
|
77
|
+
"description": 0,
|
|
78
|
+
"expression": "$1 > 10 && $2 > 10",
|
|
79
|
+
"type": "review",
|
|
80
|
+
"assignee": "homer@example.com",
|
|
81
|
+
"tags": ["foo", "bar"],
|
|
82
|
+
"channel_references": [
|
|
83
|
+
{"$1": {"name": "voltage", "data_type": "double"}},
|
|
84
|
+
{"$2": {"name": "vehicle_state", "data_type": "double"}},
|
|
85
|
+
],
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
with pytest.raises(
|
|
90
|
+
YamlConfigError, match="Expected 'expression' to be <<class 'str'> | <class 'dict'>>"
|
|
91
|
+
):
|
|
92
|
+
rule._validate_rule(
|
|
93
|
+
{
|
|
94
|
+
"name": "overheat_rule",
|
|
95
|
+
"description": "some_description",
|
|
96
|
+
"expression": 123,
|
|
97
|
+
"type": "review",
|
|
98
|
+
"assignee": "homer@example.com",
|
|
99
|
+
"tags": ["foo", "bar"],
|
|
100
|
+
"channel_references": [
|
|
101
|
+
{"$1": {"name": "voltage", "data_type": "double"}},
|
|
102
|
+
{"$2": {"name": "vehicle_state", "data_type": "double"}},
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
with pytest.raises(
|
|
108
|
+
YamlConfigError, match="Expected 'type' to be <review | phase> but it is <str>"
|
|
109
|
+
):
|
|
110
|
+
rule._validate_rule(
|
|
111
|
+
{
|
|
112
|
+
"name": "overheat_rule",
|
|
113
|
+
"description": "foobar",
|
|
114
|
+
"expression": "$1 > 10 && $2 > 10",
|
|
115
|
+
"type": "not_valid_type",
|
|
116
|
+
"assignee": "homer@example.com",
|
|
117
|
+
"tags": ["foo", "bar"],
|
|
118
|
+
"channel_references": [
|
|
119
|
+
{"$1": {"name": "voltage", "data_type": "double"}},
|
|
120
|
+
{"$2": {"name": "vehicle_state", "data_type": "double"}},
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
with pytest.raises(YamlConfigError, match="Expected 'tags' to be"):
|
|
126
|
+
rule._validate_rule(
|
|
127
|
+
{
|
|
128
|
+
"name": "overheat_rule",
|
|
129
|
+
"description": "foobar",
|
|
130
|
+
"expression": "$1 > 10 && $2 > 10",
|
|
131
|
+
"type": "phase",
|
|
132
|
+
"assignee": "homer@example.com",
|
|
133
|
+
"tags": 123,
|
|
134
|
+
"channel_references": [
|
|
135
|
+
{"$1": {"name": "voltage", "data_type": "double"}},
|
|
136
|
+
{"$2": {"name": "vehicle_state", "data_type": "double"}},
|
|
137
|
+
],
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
with pytest.raises(YamlConfigError, match="Invalid channel reference key"):
|
|
142
|
+
rule._validate_rule(
|
|
143
|
+
{
|
|
144
|
+
"name": "overheat_rule",
|
|
145
|
+
"description": "foobar",
|
|
146
|
+
"expression": "$1 > 10 && $2 > 10",
|
|
147
|
+
"type": "phase",
|
|
148
|
+
"assignee": "homer@example.com",
|
|
149
|
+
"channel_references": [
|
|
150
|
+
{"$foo": {"name": "voltage", "data_type": "double"}},
|
|
151
|
+
{"$2": {"name": "vehicle_state", "data_type": "double"}},
|
|
152
|
+
],
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
with pytest.raises(YamlConfigError, match="Invalid sub-expression key"):
|
|
157
|
+
rule._validate_rule(
|
|
158
|
+
{
|
|
159
|
+
"name": "overheat_rule",
|
|
160
|
+
"description": "foobar",
|
|
161
|
+
"expression": {"name": "kinetic_energy_gt"},
|
|
162
|
+
"type": "phase",
|
|
163
|
+
"assignee": "homer@example.com",
|
|
164
|
+
"channel_references": [
|
|
165
|
+
{"$1": {"name": "velocity", "data_type": "double"}},
|
|
166
|
+
],
|
|
167
|
+
"sub_expressions": [
|
|
168
|
+
{"mass": None},
|
|
169
|
+
],
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
with pytest.raises(YamlConfigError, match="Expected 'asset_names' to be"):
|
|
174
|
+
rule._validate_rule(
|
|
175
|
+
{
|
|
176
|
+
"name": "overheat_rule",
|
|
177
|
+
"description": "some_description",
|
|
178
|
+
"expression": "$1 > 10 && $2 > 10",
|
|
179
|
+
"type": "review",
|
|
180
|
+
"assignee": "homer@example.com",
|
|
181
|
+
"tags": ["foo", "bar"],
|
|
182
|
+
"channel_references": [
|
|
183
|
+
{"$1": {"name": "voltage", "data_type": "double"}},
|
|
184
|
+
{"$2": {"name": "vehicle_state", "data_type": "double"}},
|
|
185
|
+
],
|
|
186
|
+
"rule_client_key": "overheat_rule_key",
|
|
187
|
+
"asset_names": 123,
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
with pytest.raises(YamlConfigError, match="Expected 'tag_names' to be"):
|
|
192
|
+
rule._validate_rule(
|
|
193
|
+
{
|
|
194
|
+
"name": "overheat_rule",
|
|
195
|
+
"description": "some_description",
|
|
196
|
+
"expression": "$1 > 10 && $2 > 10",
|
|
197
|
+
"type": "review",
|
|
198
|
+
"assignee": "homer@example.com",
|
|
199
|
+
"tags": ["foo", "bar"],
|
|
200
|
+
"channel_references": [
|
|
201
|
+
{"$1": {"name": "voltage", "data_type": "double"}},
|
|
202
|
+
{"$2": {"name": "vehicle_state", "data_type": "double"}},
|
|
203
|
+
],
|
|
204
|
+
"rule_client_key": "overheat_rule_key",
|
|
205
|
+
"tag_names": 123,
|
|
206
|
+
}
|
|
207
|
+
)
|
sift_py/yaml/channel.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Dict, List, Literal, Union, cast
|
|
5
|
+
|
|
6
|
+
from typing_extensions import NotRequired, TypedDict
|
|
7
|
+
|
|
8
|
+
from sift_py.ingestion.channel import ChannelDataTypeStrRep
|
|
9
|
+
from sift_py.ingestion.config.yaml.error import YamlConfigError
|
|
10
|
+
from sift_py.yaml.utils import _type_fqn
|
|
11
|
+
|
|
12
|
+
_CHANNEL_REFERENCE_REGEX = re.compile(r"^\$\d+$")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _validate_channel_anchor(val: Any):
|
|
16
|
+
if not isinstance(val, str):
|
|
17
|
+
raise YamlConfigError._invalid_property(
|
|
18
|
+
val,
|
|
19
|
+
"<str>",
|
|
20
|
+
"&str",
|
|
21
|
+
["channels"],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _validate_channel(val: Any):
|
|
26
|
+
channel = cast(Dict[Any, Any], val)
|
|
27
|
+
|
|
28
|
+
name = channel.get("name")
|
|
29
|
+
|
|
30
|
+
if not isinstance(name, str):
|
|
31
|
+
raise YamlConfigError._invalid_property(name, "- name", "str", ["channels"])
|
|
32
|
+
|
|
33
|
+
description = channel.get("description")
|
|
34
|
+
|
|
35
|
+
if description is not None and not isinstance(description, str):
|
|
36
|
+
raise YamlConfigError._invalid_property(description, "- description", "str", ["channels"])
|
|
37
|
+
|
|
38
|
+
unit = channel.get("unit")
|
|
39
|
+
|
|
40
|
+
if unit is not None and not isinstance(unit, str):
|
|
41
|
+
raise YamlConfigError._invalid_property(unit, "- unit", "str", ["channels"])
|
|
42
|
+
|
|
43
|
+
component = channel.get("component")
|
|
44
|
+
|
|
45
|
+
if component is not None and not isinstance(component, str):
|
|
46
|
+
raise YamlConfigError._invalid_property(component, "- component", "str", ["channels"])
|
|
47
|
+
|
|
48
|
+
data_type = channel.get("data_type")
|
|
49
|
+
valid_data_type_values = [v.value for v in ChannelDataTypeStrRep]
|
|
50
|
+
|
|
51
|
+
if not data_type in valid_data_type_values:
|
|
52
|
+
raise YamlConfigError._invalid_property(
|
|
53
|
+
data_type,
|
|
54
|
+
"- data_type",
|
|
55
|
+
" | ".join(valid_data_type_values),
|
|
56
|
+
["channels"],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if data_type == ChannelDataTypeStrRep.ENUM.value:
|
|
60
|
+
enum_types = channel.get("enum_types")
|
|
61
|
+
|
|
62
|
+
if not isinstance(enum_types, list):
|
|
63
|
+
raise YamlConfigError._invalid_property(
|
|
64
|
+
enum_types,
|
|
65
|
+
"- enum_types",
|
|
66
|
+
f"List<{_type_fqn(ChannelEnumTypeYamlSpec)}>",
|
|
67
|
+
["channels"],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
for enum_type in cast(List[Any], enum_types):
|
|
71
|
+
_validate_enum_type(enum_type)
|
|
72
|
+
|
|
73
|
+
elif data_type == ChannelDataTypeStrRep.BIT_FIELD.value:
|
|
74
|
+
bit_field_elements = channel.get("bit_field_elements")
|
|
75
|
+
|
|
76
|
+
if not isinstance(bit_field_elements, list):
|
|
77
|
+
raise YamlConfigError._invalid_property(
|
|
78
|
+
bit_field_elements,
|
|
79
|
+
"- bit_field_elements",
|
|
80
|
+
f"List<{_type_fqn(ChannelBitFieldElementYamlSpec)}>",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
for bit_field_element in cast(List[Any], bit_field_elements):
|
|
84
|
+
_validate_bit_field_element(bit_field_element)
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
enum_types = channel.get("enum_types")
|
|
88
|
+
|
|
89
|
+
if enum_types is not None:
|
|
90
|
+
raise YamlConfigError(
|
|
91
|
+
f"Channel of data-type '{data_type}' should not have 'enum_types' set."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
bit_field_elements = channel.get("bit_field_elements")
|
|
95
|
+
|
|
96
|
+
if bit_field_elements is not None:
|
|
97
|
+
raise YamlConfigError(
|
|
98
|
+
f"Channel of data-type '{data_type}' should not have 'bit_field_elements' set."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _validate_enum_type(val: Any):
|
|
103
|
+
enum_type = cast(Dict[Any, Any], val)
|
|
104
|
+
|
|
105
|
+
name = enum_type.get("name")
|
|
106
|
+
|
|
107
|
+
if not isinstance(name, str):
|
|
108
|
+
raise YamlConfigError._invalid_property(
|
|
109
|
+
name,
|
|
110
|
+
"- name",
|
|
111
|
+
"str",
|
|
112
|
+
["channels", "- enum_type"],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
key = enum_type.get("key")
|
|
116
|
+
|
|
117
|
+
if not isinstance(key, int):
|
|
118
|
+
raise YamlConfigError._invalid_property(
|
|
119
|
+
key,
|
|
120
|
+
"- key",
|
|
121
|
+
"int",
|
|
122
|
+
["channels", "- enum_type"],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _validate_bit_field_element(val: Any):
|
|
127
|
+
bit_field_element = cast(Dict[Any, Any], val)
|
|
128
|
+
|
|
129
|
+
name = bit_field_element.get("name")
|
|
130
|
+
|
|
131
|
+
if not isinstance(name, str):
|
|
132
|
+
raise YamlConfigError._invalid_property(
|
|
133
|
+
name, "- name", "str", ["channels", "- bit_field_elements"]
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
index = bit_field_element.get("index")
|
|
137
|
+
|
|
138
|
+
if not isinstance(index, int):
|
|
139
|
+
raise YamlConfigError._invalid_property(
|
|
140
|
+
name, "- index", "int", ["channels", "- bit_field_elements"]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
bit_count = bit_field_element.get("bit_count")
|
|
144
|
+
|
|
145
|
+
if not isinstance(bit_count, int):
|
|
146
|
+
raise YamlConfigError._invalid_property(
|
|
147
|
+
name, "- bit_count", "int", ["channels", "- bit_field_elements"]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _validate_channel_reference(val: Any):
|
|
152
|
+
channel_reference = cast(Dict[Any, Any], val)
|
|
153
|
+
|
|
154
|
+
for key, value in channel_reference.items():
|
|
155
|
+
if not isinstance(key, str):
|
|
156
|
+
raise YamlConfigError._invalid_property(
|
|
157
|
+
channel_reference,
|
|
158
|
+
"- <str>",
|
|
159
|
+
f"Dict[str, {_type_fqn(ChannelConfigYamlSpec)}]",
|
|
160
|
+
["rules", "- channel_references"],
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if _CHANNEL_REFERENCE_REGEX.match(key) is None:
|
|
164
|
+
raise YamlConfigError(
|
|
165
|
+
f"Invalid channel reference key '{key}'. Expected an integer prefixed with '$' e.g. '$1', '$2', and so on."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if isinstance(value, dict): # Do this for YamlConfigChannelSpec but not str
|
|
169
|
+
try:
|
|
170
|
+
_validate_channel(value)
|
|
171
|
+
except YamlConfigError as err:
|
|
172
|
+
raise YamlConfigError(f"Rule '{key}' contains an invalid channel reference:\n{err}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ChannelConfigYamlSpec(TypedDict):
|
|
176
|
+
"""
|
|
177
|
+
Formal spec that defines what a channel should look like in YAML.
|
|
178
|
+
|
|
179
|
+
`name`: Name of channel.
|
|
180
|
+
`description`: Optional channel description.
|
|
181
|
+
`unit`: Unit of measurement.
|
|
182
|
+
`component`: Name of component that channel belongs to.
|
|
183
|
+
`data_type`: Type of the data associated with the channel.
|
|
184
|
+
`enum_types`: Required if `data_type` is `enum.
|
|
185
|
+
`bit_field_elements`: Required if `data_type` is `bit_field`.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
name: str
|
|
189
|
+
description: NotRequired[str]
|
|
190
|
+
unit: NotRequired[str]
|
|
191
|
+
component: NotRequired[str]
|
|
192
|
+
data_type: Union[
|
|
193
|
+
Literal["double"],
|
|
194
|
+
Literal["string"],
|
|
195
|
+
Literal["enum"],
|
|
196
|
+
Literal["bit_field"],
|
|
197
|
+
Literal["bool"],
|
|
198
|
+
Literal["float"],
|
|
199
|
+
Literal["int32"],
|
|
200
|
+
Literal["int64"],
|
|
201
|
+
Literal["uint32"],
|
|
202
|
+
Literal["uint64"],
|
|
203
|
+
]
|
|
204
|
+
enum_types: NotRequired[List[ChannelEnumTypeYamlSpec]]
|
|
205
|
+
bit_field_elements: NotRequired[List[ChannelBitFieldElementYamlSpec]]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class ChannelEnumTypeYamlSpec(TypedDict):
|
|
209
|
+
"""
|
|
210
|
+
Formal spec that defines what a channel enum type should look like in YAML.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
name: str
|
|
214
|
+
key: int
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class ChannelBitFieldElementYamlSpec(TypedDict):
|
|
218
|
+
"""
|
|
219
|
+
Formal spec that defines what a bit-field element should look like in YAML.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
name: str
|
|
223
|
+
index: int
|
|
224
|
+
bit_count: int
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, List, cast
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from typing_extensions import NotRequired, TypedDict
|
|
7
|
+
|
|
8
|
+
from sift_py.ingestion.config.yaml.error import YamlConfigError
|
|
9
|
+
from sift_py.report_templates.config import ReportTemplateConfig
|
|
10
|
+
from sift_py.yaml.utils import _handle_subdir
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_report_templates(paths: List[Path]) -> List[ReportTemplateConfig]:
|
|
14
|
+
"""
|
|
15
|
+
Takes in a list of paths to YAML files which contains report templates and processes them into a list of
|
|
16
|
+
`ReportTemplateConfig` objects. For more information on report templates see
|
|
17
|
+
`sift_py.report_templates.config.ReportTemplateConfig`.
|
|
18
|
+
"""
|
|
19
|
+
report_templates: List[ReportTemplateConfig] = []
|
|
20
|
+
|
|
21
|
+
def update_report_templates(path: Path):
|
|
22
|
+
report_templates.extend(_read_report_template_yaml(path))
|
|
23
|
+
|
|
24
|
+
for path in paths:
|
|
25
|
+
if path.is_dir():
|
|
26
|
+
_handle_subdir(path, update_report_templates)
|
|
27
|
+
elif path.is_file():
|
|
28
|
+
update_report_templates(path)
|
|
29
|
+
return report_templates
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _read_report_template_yaml(path: Path) -> List[ReportTemplateConfig]:
|
|
33
|
+
report_templates = []
|
|
34
|
+
with open(path, "r") as f:
|
|
35
|
+
report_templates_yaml = cast(Dict[str, Any], yaml.safe_load(f.read()))
|
|
36
|
+
|
|
37
|
+
report_template_list = report_templates_yaml.get("report_templates")
|
|
38
|
+
if not isinstance(report_template_list, list):
|
|
39
|
+
raise YamlConfigError(
|
|
40
|
+
f"Expected 'report_templates' to be a list in report template yaml: '{path}'"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
for report_template in report_template_list:
|
|
44
|
+
try:
|
|
45
|
+
report_template_config = ReportTemplateConfig(**report_template)
|
|
46
|
+
report_templates.append(report_template_config)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise YamlConfigError(f"Error parsing report template '{report_template}'") from e
|
|
49
|
+
|
|
50
|
+
return report_templates
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ReportTemplateYamlSpec(TypedDict):
|
|
54
|
+
"""
|
|
55
|
+
Formal spec for a report template.
|
|
56
|
+
|
|
57
|
+
`name`: Name of the report template.
|
|
58
|
+
`template_client_key`: Unique client key to identify the report template.
|
|
59
|
+
`organization_id`: Organization ID that the report template belongs to.
|
|
60
|
+
`tags`: Tags to associate with the report template.
|
|
61
|
+
`description`: Description of the report template.
|
|
62
|
+
`rule_client_keys`: List of rule client keys associated with the report template.
|
|
63
|
+
`archived_date`: Date when the report template was archived. Setting this field
|
|
64
|
+
will archive the report template, and unsetting it will unarchive the report template.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
name: str
|
|
68
|
+
template_client_key: str
|
|
69
|
+
organization_id: NotRequired[str]
|
|
70
|
+
tags: NotRequired[List[str]]
|
|
71
|
+
description: NotRequired[str]
|
|
72
|
+
rule_client_keys: List[str]
|
|
73
|
+
archived_date: NotRequired[datetime]
|