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,34 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pydantic_core import ValidationError
|
|
3
|
+
|
|
4
|
+
from sift_py.report_templates.config import ReportTemplateConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture
|
|
8
|
+
def report_template_dict() -> dict:
|
|
9
|
+
return {
|
|
10
|
+
"name": "report-template",
|
|
11
|
+
"template_client_key": "template-client-key",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_report_template_config(report_template_dict):
|
|
16
|
+
report_template_config = ReportTemplateConfig(**report_template_dict)
|
|
17
|
+
assert report_template_config.name == "report-template"
|
|
18
|
+
assert report_template_config.template_client_key == "template-client-key"
|
|
19
|
+
assert report_template_config.organization_id == ""
|
|
20
|
+
assert report_template_config.tags is None
|
|
21
|
+
assert report_template_config.description is None
|
|
22
|
+
assert report_template_config.rule_client_keys == []
|
|
23
|
+
assert report_template_config.archived_date is None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_report_template_config_validation(report_template_dict):
|
|
27
|
+
report_template_dict.pop("name")
|
|
28
|
+
with pytest.raises(ValidationError, match="Field required"):
|
|
29
|
+
ReportTemplateConfig(**report_template_dict)
|
|
30
|
+
|
|
31
|
+
report_template_dict["name"] = "report-template"
|
|
32
|
+
report_template_dict.pop("template_client_key")
|
|
33
|
+
with pytest.raises(ValidationError, match="Field required"):
|
|
34
|
+
ReportTemplateConfig(**report_template_dict)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from unittest import mock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from sift.report_templates.v1.report_templates_pb2 import ReportTemplate
|
|
5
|
+
|
|
6
|
+
from sift_py._internal.test_util.channel import MockChannel
|
|
7
|
+
from sift_py.report_templates.config import ReportTemplateConfig
|
|
8
|
+
from sift_py.report_templates.service import ReportTemplateService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def report_template_service():
|
|
13
|
+
return ReportTemplateService(MockChannel())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_report_template_service_get_report_template_by_client_key(report_template_service):
|
|
17
|
+
report_template_client_key = "report-template-client-key"
|
|
18
|
+
|
|
19
|
+
with mock.patch.object(
|
|
20
|
+
ReportTemplateService,
|
|
21
|
+
"_get_report_template_by_client_key",
|
|
22
|
+
return_value=ReportTemplate(name="abc"),
|
|
23
|
+
) as mock_get_report_template_by_client_key:
|
|
24
|
+
report_template_service.get_report_template(client_key=report_template_client_key)
|
|
25
|
+
mock_get_report_template_by_client_key.assert_called_once_with(report_template_client_key)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_report_template_service_get_report_template_by_id(report_template_service):
|
|
29
|
+
report_template_id = "report-template-id"
|
|
30
|
+
|
|
31
|
+
with mock.patch.object(
|
|
32
|
+
ReportTemplateService, "_get_report_template_by_id", return_value=ReportTemplate(name="abc")
|
|
33
|
+
) as mock_get_report_template_by_id:
|
|
34
|
+
report_template_service.get_report_template(id=report_template_id)
|
|
35
|
+
mock_get_report_template_by_id.assert_called_once_with(report_template_id)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_report_template_service_get_report_template_missing_or_both_client_key_and_id(
|
|
39
|
+
report_template_service,
|
|
40
|
+
):
|
|
41
|
+
with pytest.raises(ValueError, match="One of client_key or id must be provided"):
|
|
42
|
+
report_template_service.get_report_template()
|
|
43
|
+
with pytest.raises(ValueError, match="One of client_key or id must be provided"):
|
|
44
|
+
report_template_service.get_report_template(client_key="abc", id="abc")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_report_template_service_create_report_template(report_template_service):
|
|
48
|
+
report_template_config = ReportTemplateConfig(
|
|
49
|
+
name="report-template",
|
|
50
|
+
template_client_key="template-client-key",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
with mock.patch.object(
|
|
54
|
+
ReportTemplateService, "_create_report_template"
|
|
55
|
+
) as mock_create_report_template:
|
|
56
|
+
report_template_service.create_or_update_report_template(report_template_config)
|
|
57
|
+
mock_create_report_template.assert_called_once_with(report_template_config)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_report_template_service_update_report_template(report_template_service):
|
|
61
|
+
report_template_config = ReportTemplateConfig(
|
|
62
|
+
name="report-template",
|
|
63
|
+
template_client_key="template-client-key",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
report_template_config_update = ReportTemplateConfig(
|
|
67
|
+
name="report-template-updated",
|
|
68
|
+
template_client_key="template-client-key",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
with mock.patch.object(
|
|
72
|
+
ReportTemplateService, "_update_report_template"
|
|
73
|
+
) as mock_update_report_template:
|
|
74
|
+
with mock.patch.object(
|
|
75
|
+
ReportTemplateService, "_get_report_template_by_client_key"
|
|
76
|
+
) as mock_get_report_template_by_client_key:
|
|
77
|
+
mock_get_report_template_by_client_key.return_value = report_template_config
|
|
78
|
+
report_template_service.create_or_update_report_template(report_template_config_update)
|
|
79
|
+
mock_update_report_template.assert_called_once_with(
|
|
80
|
+
report_template_config_update, report_template_config
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_report_template_service_missing_template_client_key(report_template_service):
|
|
85
|
+
report_template_config = ReportTemplateConfig.model_construct( # Without model validation
|
|
86
|
+
name="report-template",
|
|
87
|
+
template_client_key="",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
with pytest.raises(Exception):
|
|
91
|
+
report_template_service.create_or_update_report_template(
|
|
92
|
+
report_template_config,
|
|
93
|
+
match="Report template report-template requires a template_client_key",
|
|
94
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, List, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
|
|
8
|
+
from sift_py._internal.convert.json import AsJson
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReportTemplateConfig(BaseModel, AsJson):
|
|
12
|
+
"""
|
|
13
|
+
Configuration for a report template.
|
|
14
|
+
|
|
15
|
+
- `name`: Name of the report template.
|
|
16
|
+
- `template_client_key`: Unique client key to identify the report template.
|
|
17
|
+
- `organization_id`: Organization ID that the report template belongs to.
|
|
18
|
+
- `tags`: Tags to associate with the report template.
|
|
19
|
+
- `description`: Description of the report template.
|
|
20
|
+
- `rule_client_keys`: List of rule client keys associated with the report template.
|
|
21
|
+
- `archived_date`: Date when the report template was archived. Setting this field
|
|
22
|
+
will archive the report template, and unsetting it will unarchive the report template.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
template_client_key: str
|
|
29
|
+
organization_id: str = ""
|
|
30
|
+
tags: Optional[List[str]] = None
|
|
31
|
+
description: Optional[str] = None
|
|
32
|
+
rule_client_keys: List[str] = []
|
|
33
|
+
archived_date: Optional[datetime] = None
|
|
34
|
+
|
|
35
|
+
def as_json(self) -> Any:
|
|
36
|
+
return self.model_dump_json()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional, cast
|
|
5
|
+
|
|
6
|
+
from google.protobuf.field_mask_pb2 import FieldMask
|
|
7
|
+
from sift.report_templates.v1.report_templates_pb2 import (
|
|
8
|
+
CreateReportTemplateRequest,
|
|
9
|
+
CreateReportTemplateRequestClientKeys,
|
|
10
|
+
GetReportTemplateRequest,
|
|
11
|
+
GetReportTemplateResponse,
|
|
12
|
+
ReportTemplate,
|
|
13
|
+
ReportTemplateRule,
|
|
14
|
+
ReportTemplateTag,
|
|
15
|
+
UpdateReportTemplateRequest,
|
|
16
|
+
)
|
|
17
|
+
from sift.report_templates.v1.report_templates_pb2_grpc import ReportTemplateServiceStub
|
|
18
|
+
|
|
19
|
+
from sift_py._internal.time import to_timestamp_pb
|
|
20
|
+
from sift_py.grpc.transport import SiftChannel
|
|
21
|
+
from sift_py.report_templates.config import ReportTemplateConfig
|
|
22
|
+
from sift_py.yaml.report_templates import load_report_templates
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ReportTemplateService:
|
|
26
|
+
"""
|
|
27
|
+
A service for managing report templates. Allows for creating, updating, and retrieving report
|
|
28
|
+
templates.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_report_template_service_stub: ReportTemplateServiceStub
|
|
32
|
+
|
|
33
|
+
def __init__(self, channel: SiftChannel):
|
|
34
|
+
self._report_template_service_stub = ReportTemplateServiceStub(channel)
|
|
35
|
+
|
|
36
|
+
def create_or_update_report_template(self, config: ReportTemplateConfig):
|
|
37
|
+
"""
|
|
38
|
+
Create or update a report template via a ReportTemplateConfig. The config must contain a
|
|
39
|
+
template_client_key, otherwise an exception will be raised. If a report template with the
|
|
40
|
+
same client key exists, it will be updated. Otherwise, a new report template will be created.
|
|
41
|
+
See `sift_py.report_templates.config.ReportTemplateConfig` for more information on available
|
|
42
|
+
fields to configure.
|
|
43
|
+
"""
|
|
44
|
+
if not config.template_client_key:
|
|
45
|
+
raise Exception(f"Report template {config.name} requires a template_client_key")
|
|
46
|
+
|
|
47
|
+
report_template = self._get_report_template_by_client_key(config.template_client_key)
|
|
48
|
+
if report_template:
|
|
49
|
+
self._update_report_template(config, report_template)
|
|
50
|
+
else:
|
|
51
|
+
self._create_report_template(config)
|
|
52
|
+
|
|
53
|
+
def get_report_template(
|
|
54
|
+
self, client_key: Optional[str] = None, id: Optional[str] = None
|
|
55
|
+
) -> Optional[ReportTemplateConfig]:
|
|
56
|
+
"""
|
|
57
|
+
Retrieve a report template by client key or id. Only one of client_key or id should be
|
|
58
|
+
provided, otherwise an exception will be raised. If a report template is found, it will be
|
|
59
|
+
returned as a ReportTemplateConfig object.
|
|
60
|
+
"""
|
|
61
|
+
report_template = None
|
|
62
|
+
if (not client_key and not id) or (client_key and id):
|
|
63
|
+
raise ValueError("One of client_key or id must be provided")
|
|
64
|
+
|
|
65
|
+
if id:
|
|
66
|
+
report_template = self._get_report_template_by_id(id)
|
|
67
|
+
elif client_key:
|
|
68
|
+
report_template = self._get_report_template_by_client_key(client_key)
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
ReportTemplateConfig(
|
|
72
|
+
name=report_template.name,
|
|
73
|
+
template_client_key=report_template.client_key,
|
|
74
|
+
organization_id=report_template.organization_id,
|
|
75
|
+
tags=[tag.tag_name for tag in report_template.tags],
|
|
76
|
+
description=report_template.description,
|
|
77
|
+
rule_client_keys=[rule.client_key for rule in report_template.rules],
|
|
78
|
+
archived_date=report_template.archived_date.ToDatetime()
|
|
79
|
+
if report_template.archived_date
|
|
80
|
+
else None,
|
|
81
|
+
)
|
|
82
|
+
if report_template
|
|
83
|
+
else None
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def load_report_templates_from_yaml(self, paths: List[Path]) -> List[ReportTemplateConfig]:
|
|
87
|
+
"""
|
|
88
|
+
Load report templates from YAML definitions. The YAML defined report template must have
|
|
89
|
+
a client key. If the report template with the given client key exists, it will be updated,
|
|
90
|
+
otherwise a new report template will be created.
|
|
91
|
+
See `sift_py.yaml.report_templates.load_report_templates` for more information on the YAML
|
|
92
|
+
spec for report templates.
|
|
93
|
+
"""
|
|
94
|
+
report_templates = load_report_templates(paths)
|
|
95
|
+
[
|
|
96
|
+
self.create_or_update_report_template(report_template)
|
|
97
|
+
for report_template in report_templates
|
|
98
|
+
]
|
|
99
|
+
return report_templates
|
|
100
|
+
|
|
101
|
+
def _get_report_template_by_id(self, report_template_id: str) -> Optional[ReportTemplate]:
|
|
102
|
+
req = GetReportTemplateRequest(report_template_id=report_template_id)
|
|
103
|
+
try:
|
|
104
|
+
res = cast(
|
|
105
|
+
GetReportTemplateResponse, self._report_template_service_stub.GetReportTemplate(req)
|
|
106
|
+
)
|
|
107
|
+
return cast(ReportTemplate, res.report_template) or None
|
|
108
|
+
except:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def _get_report_template_by_client_key(self, client_key: str) -> Optional[ReportTemplate]:
|
|
112
|
+
req = GetReportTemplateRequest(client_key=client_key)
|
|
113
|
+
try:
|
|
114
|
+
res = cast(
|
|
115
|
+
GetReportTemplateResponse, self._report_template_service_stub.GetReportTemplate(req)
|
|
116
|
+
)
|
|
117
|
+
return res.report_template or None
|
|
118
|
+
except:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def _create_report_template(self, config: ReportTemplateConfig):
|
|
122
|
+
client_keys_req = CreateReportTemplateRequestClientKeys(
|
|
123
|
+
rule_client_keys=config.rule_client_keys
|
|
124
|
+
)
|
|
125
|
+
req = CreateReportTemplateRequest(
|
|
126
|
+
name=config.name,
|
|
127
|
+
client_key=config.template_client_key,
|
|
128
|
+
description=config.description,
|
|
129
|
+
tag_names=config.tags,
|
|
130
|
+
organization_id=config.organization_id,
|
|
131
|
+
rule_client_keys=client_keys_req,
|
|
132
|
+
)
|
|
133
|
+
self._report_template_service_stub.CreateReportTemplate(req)
|
|
134
|
+
|
|
135
|
+
def _update_report_template(
|
|
136
|
+
self, config: ReportTemplateConfig, report_template: ReportTemplate
|
|
137
|
+
):
|
|
138
|
+
"""
|
|
139
|
+
Uses the report template id, organization id, and client key from the existing report
|
|
140
|
+
template. Updates the name, description, tags, rules, and archived date from the config,
|
|
141
|
+
if they are provided. Always passes all fields to the field mask for updating.
|
|
142
|
+
"""
|
|
143
|
+
tags = []
|
|
144
|
+
if config.tags:
|
|
145
|
+
tags = [ReportTemplateTag(tag_name=tag) for tag in config.tags]
|
|
146
|
+
|
|
147
|
+
archived_date = None
|
|
148
|
+
if config.archived_date:
|
|
149
|
+
archived_date = to_timestamp_pb(config.archived_date)
|
|
150
|
+
|
|
151
|
+
rules = [
|
|
152
|
+
ReportTemplateRule(client_key=client_key) for client_key in config.rule_client_keys
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
updated_report_template = ReportTemplate(
|
|
156
|
+
report_template_id=report_template.report_template_id,
|
|
157
|
+
organization_id=report_template.organization_id,
|
|
158
|
+
client_key=report_template.client_key,
|
|
159
|
+
name=config.name,
|
|
160
|
+
description=config.description,
|
|
161
|
+
tags=tags,
|
|
162
|
+
rules=rules,
|
|
163
|
+
archived_date=archived_date,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
field_mask = FieldMask(paths=["name", "description", "tags", "rules", "archived_date"])
|
|
167
|
+
self._report_template_service_stub.UpdateReportTemplate(
|
|
168
|
+
UpdateReportTemplateRequest(
|
|
169
|
+
report_template=updated_report_template, update_mask=field_mask
|
|
170
|
+
)
|
|
171
|
+
)
|
sift_py/rest.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import TypedDict
|
|
2
|
+
|
|
3
|
+
from typing_extensions import NotRequired
|
|
4
|
+
|
|
5
|
+
from sift_py.grpc.transport import _clean_uri
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SiftRestConfig(TypedDict):
|
|
9
|
+
"""
|
|
10
|
+
Config class used to to interact with services that use Sift's REST API.`.
|
|
11
|
+
- `uri`: The URI of Sift's REST API. The scheme portion of the URI i.e. `https://` should be ommitted.
|
|
12
|
+
- `apikey`: User-generated API key generated via the Sift application.
|
|
13
|
+
- `use_ssl`: INTERNAL USE. Meant to be used for local development.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
uri: str
|
|
17
|
+
apikey: str
|
|
18
|
+
use_ssl: NotRequired[bool]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def compute_uri(restconf: SiftRestConfig) -> str:
|
|
22
|
+
uri = restconf["uri"]
|
|
23
|
+
use_ssl = restconf.get("use_ssl", True)
|
|
24
|
+
clean_uri = _clean_uri(uri, use_ssl)
|
|
25
|
+
|
|
26
|
+
if use_ssl:
|
|
27
|
+
return f"https://{clean_uri}"
|
|
28
|
+
|
|
29
|
+
return f"http://{clean_uri}"
|
sift_py/rule/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from sift_py.ingestion.channel import ChannelConfig, ChannelDataType
|
|
2
|
+
|
|
3
|
+
from .config import (
|
|
4
|
+
RuleActionCreateDataReviewAnnotation,
|
|
5
|
+
RuleActionCreatePhaseAnnotation,
|
|
6
|
+
RuleConfig,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_rule_config_json():
|
|
11
|
+
voltage_rule_expression = "$1 > 10"
|
|
12
|
+
voltage_rule_config = RuleConfig(
|
|
13
|
+
name="High Voltage",
|
|
14
|
+
description="Rock & Roll",
|
|
15
|
+
expression=voltage_rule_expression,
|
|
16
|
+
action=RuleActionCreatePhaseAnnotation(),
|
|
17
|
+
channel_references=[
|
|
18
|
+
{
|
|
19
|
+
"channel_reference": "$1",
|
|
20
|
+
"channel_config": ChannelConfig(
|
|
21
|
+
name="voltage",
|
|
22
|
+
data_type=ChannelDataType.DOUBLE,
|
|
23
|
+
),
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
)
|
|
27
|
+
assert voltage_rule_config.expression == voltage_rule_expression
|
|
28
|
+
|
|
29
|
+
overheating_rule_expression = '$1 == "Accelerating" && $2 > $3'
|
|
30
|
+
overheating_rule_config = RuleConfig(
|
|
31
|
+
name="overheating",
|
|
32
|
+
description="checks if vehicle overheats while accelerating",
|
|
33
|
+
expression=overheating_rule_expression,
|
|
34
|
+
action=RuleActionCreateDataReviewAnnotation(
|
|
35
|
+
tags=["foo", "bar"],
|
|
36
|
+
assignee="foobar@baz.com",
|
|
37
|
+
),
|
|
38
|
+
channel_references=[
|
|
39
|
+
{
|
|
40
|
+
"channel_reference": "$1",
|
|
41
|
+
"channel_config": ChannelConfig(
|
|
42
|
+
name="vehicle_state",
|
|
43
|
+
component="motor",
|
|
44
|
+
data_type=ChannelDataType.INT_32,
|
|
45
|
+
),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"channel_reference": "$2",
|
|
49
|
+
"channel_config": ChannelConfig(
|
|
50
|
+
name="temperature",
|
|
51
|
+
component="motor",
|
|
52
|
+
data_type=ChannelDataType.INT_32,
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
sub_expressions={
|
|
57
|
+
"$3": 80,
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
assert overheating_rule_config.expression == '$1 == "Accelerating" && $2 > 80'
|
|
61
|
+
|
|
62
|
+
contains_rule_expression = "contains($1, $2)"
|
|
63
|
+
contains_rule_config = RuleConfig(
|
|
64
|
+
name="contains",
|
|
65
|
+
description="checks if vehicle overheats while accelerating",
|
|
66
|
+
expression=contains_rule_expression,
|
|
67
|
+
action=RuleActionCreateDataReviewAnnotation(
|
|
68
|
+
tags=["foo", "bar"],
|
|
69
|
+
assignee="foobar@baz.com",
|
|
70
|
+
),
|
|
71
|
+
channel_references=[
|
|
72
|
+
{
|
|
73
|
+
"channel_reference": "$1",
|
|
74
|
+
"channel_config": ChannelConfig(
|
|
75
|
+
name="log",
|
|
76
|
+
data_type=ChannelDataType.INT_32,
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
sub_expressions={
|
|
81
|
+
"$2": "Error",
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
assert contains_rule_config.expression == 'contains($1, "Error")'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_rule_named_expressions():
|
|
88
|
+
kinetic_energy_gt_expression = "0.5 * $mass * $1 * $1 > $threshold"
|
|
89
|
+
|
|
90
|
+
rule_on_kinetic_energy = RuleConfig(
|
|
91
|
+
name="rule_onkinetic_energy",
|
|
92
|
+
description="checks high periods of energy output",
|
|
93
|
+
expression=kinetic_energy_gt_expression,
|
|
94
|
+
action=RuleActionCreatePhaseAnnotation(),
|
|
95
|
+
channel_references=[
|
|
96
|
+
{
|
|
97
|
+
"channel_reference": "$1",
|
|
98
|
+
"channel_config": ChannelConfig(
|
|
99
|
+
name="velocity",
|
|
100
|
+
data_type=ChannelDataType.INT_32,
|
|
101
|
+
),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
sub_expressions={
|
|
105
|
+
"$mass": 10,
|
|
106
|
+
"$threshold": 35,
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
assert rule_on_kinetic_energy.expression == "0.5 * 10 * $1 * $1 > 35"
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from unittest import mock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from sift.assets.v1.assets_pb2 import Asset
|
|
5
|
+
from sift.rules.v1.rules_pb2 import Rule
|
|
6
|
+
|
|
7
|
+
from sift_py._internal.test_util.channel import MockChannel
|
|
8
|
+
from sift_py.rule.config import RuleActionCreateDataReviewAnnotation, RuleConfig
|
|
9
|
+
from sift_py.rule.service import RuleService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture()
|
|
13
|
+
def rule_service():
|
|
14
|
+
return RuleService(MockChannel())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_rule_service_get_rule_from_client_key(rule_service):
|
|
18
|
+
rule_client_key = "rule-client-key"
|
|
19
|
+
|
|
20
|
+
with mock.patch.object(
|
|
21
|
+
RuleService,
|
|
22
|
+
"_get_rule_from_client_key",
|
|
23
|
+
return_value=Rule(name="abc"),
|
|
24
|
+
) as mock_get_rule_from_client_key:
|
|
25
|
+
rule_service.get_rule(rule_client_key)
|
|
26
|
+
mock_get_rule_from_client_key.assert_called_once_with(rule_client_key)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_rule_service_get_rule_from_rule_id(rule_service):
|
|
30
|
+
rule_id = "rule-id"
|
|
31
|
+
|
|
32
|
+
with mock.patch.object(
|
|
33
|
+
RuleService, "_get_rule_from_rule_id", return_value=Rule(name="abc")
|
|
34
|
+
) as mock_get_rule_from_rule_id:
|
|
35
|
+
rule_service.get_rule(rule_id)
|
|
36
|
+
mock_get_rule_from_rule_id.assert_called_once_with(rule_id)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_rule_service_get_rule_missing_client_key_and_id(rule_service):
|
|
40
|
+
with mock.patch.object(
|
|
41
|
+
RuleService,
|
|
42
|
+
"_get_rule_from_client_key",
|
|
43
|
+
return_value=None,
|
|
44
|
+
) as mock_get_rule_from_client_key:
|
|
45
|
+
with mock.patch.object(
|
|
46
|
+
RuleService, "_get_rule_from_rule_id", return_value=None
|
|
47
|
+
) as mock_get_rule_from_rule_id:
|
|
48
|
+
rule = rule_service.get_rule("")
|
|
49
|
+
mock_get_rule_from_client_key.asset_called_once()
|
|
50
|
+
mock_get_rule_from_rule_id.asset_called_once()
|
|
51
|
+
assert rule is None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_rule_service_create_rule(rule_service):
|
|
55
|
+
rule = RuleConfig(
|
|
56
|
+
name="rule",
|
|
57
|
+
rule_client_key="rule-client-key",
|
|
58
|
+
channel_references=[],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
with mock.patch.object(RuleService, "_create_rule") as mock_create_rule:
|
|
62
|
+
rule_service.create_or_update_rule(rule)
|
|
63
|
+
mock_create_rule.assert_called_once_with(rule)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_rule_service_update_rule(rule_service):
|
|
67
|
+
rule = RuleConfig(
|
|
68
|
+
name="rule",
|
|
69
|
+
rule_client_key="rule-client-key",
|
|
70
|
+
channel_references=[],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with mock.patch.object(RuleService, "_update_rule") as mock_update_rule:
|
|
74
|
+
with mock.patch.object(
|
|
75
|
+
RuleService, "_get_rule_from_client_key", return_value=Rule(name=rule.name)
|
|
76
|
+
) as mock_get_rule_from_client_key:
|
|
77
|
+
rule_service.create_or_update_rule(rule)
|
|
78
|
+
mock_get_rule_from_client_key.assert_called_once_with(rule.rule_client_key)
|
|
79
|
+
mock_update_rule.assert_called_once()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_rule_service_load_rules_from_yaml(rule_service):
|
|
83
|
+
rule_yaml = {
|
|
84
|
+
"name": "rule",
|
|
85
|
+
"rule_client_key": "rule-client-key",
|
|
86
|
+
"channel_references": [{"$1": "channel"}],
|
|
87
|
+
"description": "description",
|
|
88
|
+
"expression": "$1 > 0",
|
|
89
|
+
"assignee": "assignee@abc.com",
|
|
90
|
+
"type": "review",
|
|
91
|
+
"asset_names": ["asset"],
|
|
92
|
+
}
|
|
93
|
+
with mock.patch.object(RuleService, "create_or_update_rule"):
|
|
94
|
+
with mock.patch(
|
|
95
|
+
"sift_py.rule.service.load_rule_modules",
|
|
96
|
+
return_value=[rule_yaml],
|
|
97
|
+
):
|
|
98
|
+
rule_configs = rule_service.load_rules_from_yaml(["path/to/rules.yml"])
|
|
99
|
+
assert len(rule_configs) == 1
|
|
100
|
+
|
|
101
|
+
rule_config = rule_configs[0]
|
|
102
|
+
assert rule_config.name == rule_yaml["name"]
|
|
103
|
+
assert rule_config.rule_client_key == rule_yaml["rule_client_key"]
|
|
104
|
+
assert rule_config.channel_references == [
|
|
105
|
+
{
|
|
106
|
+
"channel_reference": "$1",
|
|
107
|
+
"channel_identifier": "channel",
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
assert rule_config.description == rule_yaml["description"]
|
|
111
|
+
assert rule_config.expression == rule_yaml["expression"]
|
|
112
|
+
assert rule_config.action.assignee == rule_yaml["assignee"]
|
|
113
|
+
assert rule_config.asset_names == rule_yaml["asset_names"]
|
|
114
|
+
assert isinstance(rule_config.action, RuleActionCreateDataReviewAnnotation)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_rule_service_attach_asset():
|
|
118
|
+
rule_config = RuleConfig(
|
|
119
|
+
name="rule",
|
|
120
|
+
rule_client_key="rule-client-key",
|
|
121
|
+
expression="1",
|
|
122
|
+
channel_references=[],
|
|
123
|
+
action=RuleActionCreateDataReviewAnnotation(),
|
|
124
|
+
asset_names=["abc"],
|
|
125
|
+
)
|
|
126
|
+
asset = Asset(name="asset", asset_id="asset-id", organization_id="org-id")
|
|
127
|
+
with mock.patch("sift_py.rule.service.RuleServiceStub", return_value=mock.MagicMock()):
|
|
128
|
+
rule_service = RuleService(MockChannel())
|
|
129
|
+
with mock.patch.object(RuleService, "_get_assets", return_value=[asset]):
|
|
130
|
+
returned_config = rule_service.attach_asset(rule_config, ["asset"])
|
|
131
|
+
assert "abc" in returned_config.asset_names
|
|
132
|
+
assert "asset" in returned_config.asset_names
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_rule_service_detach_asset():
|
|
136
|
+
rule_config = RuleConfig(
|
|
137
|
+
name="rule",
|
|
138
|
+
rule_client_key="rule-client-key",
|
|
139
|
+
expression="1",
|
|
140
|
+
channel_references=[],
|
|
141
|
+
action=RuleActionCreateDataReviewAnnotation(),
|
|
142
|
+
asset_names=["abc", "def"],
|
|
143
|
+
)
|
|
144
|
+
asset_abc = Asset(name="abc", asset_id="abc-id", organization_id="org-id")
|
|
145
|
+
asset_def = Asset(name="def", asset_id="def-id", organization_id="org-id")
|
|
146
|
+
with mock.patch("sift_py.rule.service.RuleServiceStub", return_value=mock.MagicMock()):
|
|
147
|
+
rule_service = RuleService(MockChannel())
|
|
148
|
+
with mock.patch.object(RuleService, "_get_assets", return_value=[asset_abc, asset_def]):
|
|
149
|
+
returned_config = rule_service.detach_asset(rule_config, ["abc"])
|
|
150
|
+
assert "abc" not in returned_config.asset_names
|
|
151
|
+
assert "def" in returned_config.asset_names
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_rule_service_detach_asset_empty_list():
|
|
155
|
+
rule_config = RuleConfig(
|
|
156
|
+
name="rule",
|
|
157
|
+
rule_client_key="rule-client-key",
|
|
158
|
+
expression="1",
|
|
159
|
+
channel_references=[],
|
|
160
|
+
action=RuleActionCreateDataReviewAnnotation(),
|
|
161
|
+
asset_names=["asset"],
|
|
162
|
+
)
|
|
163
|
+
asset = Asset(name="asset", asset_id="asset-id", organization_id="org-id")
|
|
164
|
+
with mock.patch("sift_py.rule.service.RuleServiceStub", return_value=mock.MagicMock()):
|
|
165
|
+
rule_service = RuleService(MockChannel())
|
|
166
|
+
with mock.patch.object(RuleService, "_get_assets", return_value=[asset]):
|
|
167
|
+
with pytest.raises(ValueError, match="must be associated with at least one asset"):
|
|
168
|
+
rule_service.detach_asset(rule_config, ["asset"])
|