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/yaml/rule.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Literal, Union, cast
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from typing_extensions import NotRequired, TypedDict
|
|
9
|
+
|
|
10
|
+
from sift_py.ingestion.config.yaml.error import YamlConfigError
|
|
11
|
+
from sift_py.rule.config import RuleActionAnnotationKind
|
|
12
|
+
from sift_py.yaml.channel import (
|
|
13
|
+
ChannelConfigYamlSpec,
|
|
14
|
+
_validate_channel_reference,
|
|
15
|
+
)
|
|
16
|
+
from sift_py.yaml.utils import _handle_subdir, _type_fqn
|
|
17
|
+
|
|
18
|
+
_SUB_EXPRESSION_REGEX = re.compile(r"^\$[a-zA-Z_]+$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_named_expression_modules(paths: List[Path]) -> Dict[str, str]:
|
|
22
|
+
"""
|
|
23
|
+
Takes in a list of paths to YAML files which contains named expressions and processes them into a `dict`.
|
|
24
|
+
The key is the name of the expression and the value is the expression itself. For more information on
|
|
25
|
+
named expression modules see `sift_py/yaml/rule.py`.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
named_expressions = {}
|
|
29
|
+
|
|
30
|
+
for path in paths:
|
|
31
|
+
named_expr_module = _read_named_expression_module_yaml(path)
|
|
32
|
+
|
|
33
|
+
for name, expr in named_expr_module.items():
|
|
34
|
+
if name in named_expressions:
|
|
35
|
+
raise YamlConfigError(
|
|
36
|
+
f"Encountered expressions with identical names being loaded, '{name}'."
|
|
37
|
+
)
|
|
38
|
+
named_expressions[name] = expr
|
|
39
|
+
|
|
40
|
+
return named_expressions
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_rule_modules(paths: List[Path]) -> List[RuleYamlSpec]:
|
|
44
|
+
"""
|
|
45
|
+
Takes in a list of paths which may either be directories or files containing rule module YAML files,
|
|
46
|
+
and processes them into a `list`. For more information on rule modules see
|
|
47
|
+
RulemoduleYamlSpec in `sift_py/yaml/rule.py`.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
rule_modules: List[RuleYamlSpec] = []
|
|
51
|
+
|
|
52
|
+
def update_rule_modules(rule_module_path: Path):
|
|
53
|
+
rule_module = _read_rule_module_yaml(rule_module_path)
|
|
54
|
+
rule_modules.extend(rule_module)
|
|
55
|
+
|
|
56
|
+
for path in paths:
|
|
57
|
+
if path.is_dir():
|
|
58
|
+
_handle_subdir(path, update_rule_modules)
|
|
59
|
+
elif path.is_file():
|
|
60
|
+
update_rule_modules(path)
|
|
61
|
+
|
|
62
|
+
return rule_modules
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _read_named_expression_module_yaml(path: Path) -> Dict[str, str]:
|
|
66
|
+
with open(path, "r") as f:
|
|
67
|
+
named_expressions = cast(Dict[Any, Any], yaml.safe_load(f.read()))
|
|
68
|
+
|
|
69
|
+
for key, value in named_expressions.items():
|
|
70
|
+
if not isinstance(key, str):
|
|
71
|
+
raise YamlConfigError(
|
|
72
|
+
f"Expected '{key}' to be a string in named expression module '{path}'."
|
|
73
|
+
)
|
|
74
|
+
if not isinstance(value, str):
|
|
75
|
+
raise YamlConfigError(
|
|
76
|
+
f"Expected expression of '{key}' to be a string in named expression module '{path}'."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return cast(Dict[str, str], named_expressions)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _read_rule_module_yaml(path: Path) -> List[RuleYamlSpec]:
|
|
83
|
+
with open(path, "r") as f:
|
|
84
|
+
module_rules = cast(Dict[Any, Any], yaml.safe_load(f.read()))
|
|
85
|
+
rules = module_rules.get("rules")
|
|
86
|
+
if not isinstance(rules, list):
|
|
87
|
+
raise YamlConfigError(
|
|
88
|
+
f"Expected '{rules}' to be a list in rule module yaml: '{path}'"
|
|
89
|
+
f"{_type_fqn(RuleYamlSpec)}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
for rule in cast(List[Any], rules):
|
|
93
|
+
_validate_rule(rule)
|
|
94
|
+
|
|
95
|
+
return cast(List[RuleYamlSpec], rules)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _validate_rule(val: Any):
|
|
99
|
+
rule = cast(Dict[Any, Any], val)
|
|
100
|
+
|
|
101
|
+
name = rule.get("name")
|
|
102
|
+
|
|
103
|
+
if not isinstance(name, str):
|
|
104
|
+
raise YamlConfigError._invalid_property(name, "- name", "str", ["rules"])
|
|
105
|
+
|
|
106
|
+
channel_references = rule.get("channel_references")
|
|
107
|
+
|
|
108
|
+
if channel_references is not None:
|
|
109
|
+
if not isinstance(channel_references, list):
|
|
110
|
+
raise YamlConfigError._invalid_property(
|
|
111
|
+
channel_references,
|
|
112
|
+
"- channel_references",
|
|
113
|
+
f"List[Dict[str, {_type_fqn(ChannelConfigYamlSpec)}]]",
|
|
114
|
+
["rules"],
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
for channel_reference in cast(List[Any], channel_references):
|
|
118
|
+
_validate_channel_reference(channel_reference)
|
|
119
|
+
|
|
120
|
+
rule_client_key = rule.get("rule_client_key")
|
|
121
|
+
description = rule.get("description")
|
|
122
|
+
expression = rule.get("expression")
|
|
123
|
+
rule_type = rule.get("type")
|
|
124
|
+
assignee = rule.get("assignee")
|
|
125
|
+
tags = rule.get("tags")
|
|
126
|
+
sub_expressions = rule.get("sub_expressions")
|
|
127
|
+
asset_names = rule.get("asset_names")
|
|
128
|
+
tag_names = rule.get("tag_names")
|
|
129
|
+
|
|
130
|
+
if rule_client_key is not None and not isinstance(rule_client_key, str):
|
|
131
|
+
raise YamlConfigError._invalid_property(
|
|
132
|
+
rule_client_key, "- rule_client_key", "str", ["rules"]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if description is not None and not isinstance(description, str):
|
|
136
|
+
raise YamlConfigError._invalid_property(description, "- description", "str", ["rules"])
|
|
137
|
+
|
|
138
|
+
if isinstance(expression, dict):
|
|
139
|
+
expression_name = cast(Dict[Any, Any], expression).get("name")
|
|
140
|
+
|
|
141
|
+
if not isinstance(expression_name, str):
|
|
142
|
+
raise YamlConfigError._invalid_property(
|
|
143
|
+
expression_name,
|
|
144
|
+
"name",
|
|
145
|
+
"str",
|
|
146
|
+
["rules", "- expression"],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
elif not isinstance(expression, str):
|
|
150
|
+
raise YamlConfigError._invalid_property(
|
|
151
|
+
expression,
|
|
152
|
+
"- expression",
|
|
153
|
+
"<class 'str'> | <class 'dict'>",
|
|
154
|
+
["rules"],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
valid_rule_types = [kind.value for kind in RuleActionAnnotationKind]
|
|
158
|
+
|
|
159
|
+
if rule_type not in valid_rule_types:
|
|
160
|
+
raise YamlConfigError._invalid_property(
|
|
161
|
+
rule_type,
|
|
162
|
+
"- type",
|
|
163
|
+
" | ".join(valid_rule_types),
|
|
164
|
+
["rules"],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if assignee is not None and not isinstance(assignee, str):
|
|
168
|
+
raise YamlConfigError._invalid_property(
|
|
169
|
+
assignee,
|
|
170
|
+
"- assignee",
|
|
171
|
+
"str",
|
|
172
|
+
["rules"],
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if tags is not None and not isinstance(tags, list):
|
|
176
|
+
raise YamlConfigError._invalid_property(
|
|
177
|
+
tags,
|
|
178
|
+
"- tags",
|
|
179
|
+
"List[str]",
|
|
180
|
+
["rules"],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if sub_expressions is not None:
|
|
184
|
+
if not isinstance(channel_references, list):
|
|
185
|
+
raise YamlConfigError._invalid_property(
|
|
186
|
+
channel_references,
|
|
187
|
+
"- sub_expressions",
|
|
188
|
+
"List[Dict[str, List[Dict[str, str]]]]",
|
|
189
|
+
["rules"],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
for sub_expression in cast(List[Any], sub_expressions):
|
|
193
|
+
_validate_sub_expression(sub_expression)
|
|
194
|
+
|
|
195
|
+
if asset_names is not None and not isinstance(asset_names, list):
|
|
196
|
+
raise YamlConfigError._invalid_property(
|
|
197
|
+
asset_names,
|
|
198
|
+
"- asset_names",
|
|
199
|
+
"List[str]",
|
|
200
|
+
["rules"],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if tag_names is not None and not isinstance(tag_names, list):
|
|
204
|
+
raise YamlConfigError._invalid_property(
|
|
205
|
+
tag_names,
|
|
206
|
+
"- tag_names",
|
|
207
|
+
"List[str]",
|
|
208
|
+
["rules"],
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _validate_sub_expression(val: Any):
|
|
213
|
+
sub_expression = cast(Dict[Any, Any], val)
|
|
214
|
+
|
|
215
|
+
for key in sub_expression.keys():
|
|
216
|
+
if not isinstance(key, str):
|
|
217
|
+
raise YamlConfigError._invalid_property(
|
|
218
|
+
sub_expression,
|
|
219
|
+
"- <str>",
|
|
220
|
+
"Dict[str, Any]",
|
|
221
|
+
["rules", "- sub_expressions"],
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if _SUB_EXPRESSION_REGEX.match(key) is None:
|
|
225
|
+
raise YamlConfigError(
|
|
226
|
+
f"Invalid sub-expression key, '{key}'. Characters must be in the character set [a-zA-Z_] and prefixed with a '$'."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class RuleModuleYamlSpec(TypedDict):
|
|
231
|
+
"""
|
|
232
|
+
The formal definition of what a rule module looks like in YAML.
|
|
233
|
+
|
|
234
|
+
`rules`: A list of rules that belong to the module.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
rules: List[RuleYamlSpec]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class RuleYamlSpec(TypedDict):
|
|
241
|
+
"""
|
|
242
|
+
The formal definition of what a single rule looks like in YAML.
|
|
243
|
+
|
|
244
|
+
`name`: Name of the rule.
|
|
245
|
+
`rule_client_key`: User-defined string-key that uniquely identifies this rule config.
|
|
246
|
+
`description`: Description of rule.
|
|
247
|
+
`expression`:
|
|
248
|
+
Either an expression-string or a `sift_py.ingestion.config.yaml.spec.NamedExpressionYamlSpec` referencing a named expression.
|
|
249
|
+
`type`: Determines the action to perform if a rule gets evaluated to true.
|
|
250
|
+
`assignee`: If `type` is `review`, determines who to notify. Expects an email.
|
|
251
|
+
`tags`: Tags to associate with the rule.
|
|
252
|
+
`channel_references`: A list of channel references that maps to an actual channel. More below.
|
|
253
|
+
`sub_expressions`: A list of sub-expressions which is a mapping of place-holders to sub-expressions. Only used if using named expressions.
|
|
254
|
+
`asset_names`: A list of asset names that this rule should be applied to. ONLY VALID if defining rules outside of a telemetry config.
|
|
255
|
+
`tag_names`: A list of tag names that this rule should be applied to. ONLY VALID if defining rules outside of a telemetry config.
|
|
256
|
+
|
|
257
|
+
Channel references:
|
|
258
|
+
A channel reference is a string containing a numerical value prefixed with "$". Examples include "$1", "$2", "$11", and so on.
|
|
259
|
+
The channel reference is mapped to an actual channel config. In YAML it would look something like this:
|
|
260
|
+
|
|
261
|
+
```yaml
|
|
262
|
+
channel_references:
|
|
263
|
+
- $1: *vehicle_state_channel
|
|
264
|
+
- $2: *voltage_channel
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Sub-expressions:
|
|
268
|
+
A sub-expression is made up of two components: A reference and the actual sub-expression. The sub-expression reference is
|
|
269
|
+
a string with a "$" prepended to another string comprised of characters in the following character set: `[a-zA-Z0-9_]`.
|
|
270
|
+
This reference should be mapped to the actual sub-expression. For example, say you have kinematic equations in `kinematics.yml`,
|
|
271
|
+
and the equation you're interested in using looks like the following:
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
kinetic_energy_gt:
|
|
275
|
+
0.5 * $mass * $1 * $1 > $threshold
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
To properly use `kinetic_energy_gt` in your rule, it would look like the following:
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
rules:
|
|
282
|
+
- name: kinetic_energy
|
|
283
|
+
description: Tracks high energy output while in motion
|
|
284
|
+
type: review
|
|
285
|
+
assignee: bob@example.com
|
|
286
|
+
expression:
|
|
287
|
+
name: kinetic_energy_gt
|
|
288
|
+
channel_references:
|
|
289
|
+
- $1: *velocity_channel
|
|
290
|
+
sub_expressions:
|
|
291
|
+
- $mass: 10
|
|
292
|
+
- $threshold: 470
|
|
293
|
+
tags:
|
|
294
|
+
- nostromo
|
|
295
|
+
```
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
name: str
|
|
299
|
+
rule_client_key: NotRequired[str]
|
|
300
|
+
description: NotRequired[str]
|
|
301
|
+
expression: Union[str, NamedExpressionYamlSpec]
|
|
302
|
+
type: Union[Literal["phase"], Literal["review"]]
|
|
303
|
+
assignee: NotRequired[str]
|
|
304
|
+
tags: NotRequired[List[str]]
|
|
305
|
+
channel_references: NotRequired[List[Dict[str, ChannelConfigYamlSpec]]]
|
|
306
|
+
sub_expressions: NotRequired[List[Dict[str, str]]]
|
|
307
|
+
asset_names: NotRequired[List[str]]
|
|
308
|
+
tag_names: NotRequired[List[str]]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class NamedExpressionYamlSpec(TypedDict):
|
|
312
|
+
"""
|
|
313
|
+
A named expression. This class is the formal definition of what a named expression
|
|
314
|
+
should look like in YAML. The value of `name` may contain a mix of channel references
|
|
315
|
+
and channel identifiers.
|
|
316
|
+
|
|
317
|
+
For a formal definition of channel references and channel identifiers see the following:
|
|
318
|
+
`sift_py.ingestion.config.yaml.spec.RuleYamlSpec`.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
name: str
|
sift_py/yaml/utils.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Callable, Type
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _handle_subdir(path: Path, file_handler: Callable):
|
|
6
|
+
"""The file_handler callable must accept a Path object as its only argument."""
|
|
7
|
+
for file_in_dir in path.iterdir():
|
|
8
|
+
if file_in_dir.is_dir():
|
|
9
|
+
_handle_subdir(file_in_dir, file_handler)
|
|
10
|
+
elif file_in_dir.is_file():
|
|
11
|
+
file_handler(file_in_dir)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _type_fqn(typ: Type) -> str:
|
|
15
|
+
return f"{typ.__module__}.{typ.__name__}"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2024 Azimuth Industries Inc.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sift_stack_py
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Python client library for the Sift API
|
|
5
|
+
Maintainer-email: Sift Software Engineers <engineering@siftstack.com>
|
|
6
|
+
License: Copyright (c) 2024 Azimuth Industries Inc.
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
13
|
+
|
|
14
|
+
Project-URL: Homepage, https://github.com/sift-stack/sift/tree/main/python
|
|
15
|
+
Project-URL: Documentation, https://docs.siftstack.com/sift_py/sift_py.html
|
|
16
|
+
Project-URL: Repository, https://github.com/sift-stack/sift/tree/main/python
|
|
17
|
+
Project-URL: Changelog, https://github.com/sift-stack/sift/tree/main/python/CHANGELOG.md
|
|
18
|
+
Keywords: sift,sift-stack,siftstack,sift_py
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: grpcio ~=1.13
|
|
29
|
+
Requires-Dist: PyYAML ~=6.0
|
|
30
|
+
Requires-Dist: pandas ~=2.0
|
|
31
|
+
Requires-Dist: protobuf >=4.0
|
|
32
|
+
Requires-Dist: pydantic ~=2.0
|
|
33
|
+
Requires-Dist: pydantic-core ~=2.3
|
|
34
|
+
Requires-Dist: requests ~=2.25
|
|
35
|
+
Requires-Dist: requests-toolbelt ~=1.0
|
|
36
|
+
Requires-Dist: pandas-stubs ~=2.0
|
|
37
|
+
Requires-Dist: types-PyYAML ~=6.0
|
|
38
|
+
Requires-Dist: types-protobuf >=4.0
|
|
39
|
+
Requires-Dist: typing-extensions ~=4.6
|
|
40
|
+
Requires-Dist: types-requests ~=2.25
|
|
41
|
+
Provides-Extra: build
|
|
42
|
+
Requires-Dist: pdoc ==14.5.0 ; extra == 'build'
|
|
43
|
+
Requires-Dist: build ==1.2.1 ; extra == 'build'
|
|
44
|
+
Provides-Extra: development
|
|
45
|
+
Requires-Dist: grpcio-testing ~=1.13 ; extra == 'development'
|
|
46
|
+
Requires-Dist: mypy ==1.10.0 ; extra == 'development'
|
|
47
|
+
Requires-Dist: pyright ==1.1.386 ; extra == 'development'
|
|
48
|
+
Requires-Dist: pytest ==8.2.2 ; extra == 'development'
|
|
49
|
+
Requires-Dist: pytest-asyncio ==0.23.7 ; extra == 'development'
|
|
50
|
+
Requires-Dist: pytest-benchmark ==4.0.0 ; extra == 'development'
|
|
51
|
+
Requires-Dist: pytest-mock ==3.14.0 ; extra == 'development'
|
|
52
|
+
Requires-Dist: ruff ; extra == 'development'
|
|
53
|
+
Provides-Extra: openssl
|
|
54
|
+
Requires-Dist: pyOpenSSL <24.0.0 ; extra == 'openssl'
|
|
55
|
+
Requires-Dist: types-pyOpenSSL <24.0.0 ; extra == 'openssl'
|
|
56
|
+
Requires-Dist: cffi ~=1.14 ; extra == 'openssl'
|
|
57
|
+
Provides-Extra: tdms
|
|
58
|
+
Requires-Dist: npTDMS ~=1.9 ; extra == 'tdms'
|
|
59
|
+
|
|
60
|
+
# Sift Python
|
|
61
|
+
[](https://pypi.org/project/sift-stack-py/)
|
|
62
|
+
[](https://pypi.org/project/sift-stack-py/)
|
|
63
|
+
[](https://docs.siftstack.com/sift_py/sift_py.html)
|
|
64
|
+
|
|
65
|
+
This library offers a Python API on top of Sift's protocol buffers to ergonomically interface with the Sift gRPC API.
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
To install the Sift Python library:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
$ pip install sift-stack-py
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
Documentation can be found at [this link](https://docs.siftstack.com/sift_py/sift_py.html), however, if you need
|
|
78
|
+
to build the documentation for offline use, read on.
|
|
79
|
+
|
|
80
|
+
### Offline documentation
|
|
81
|
+
|
|
82
|
+
To build the documentation locally:
|
|
83
|
+
|
|
84
|
+
Clone the main `sift` repository:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
$ git clone https://github.com/sift-stack/sift.git
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Set your working directory to this project sub-directory:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
$ cd python
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Install [pdoc](https://pypi.org/project/pdoc/):
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
$ pip install pdoc
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Run `pdoc`.
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
$ pdoc lib/sift_py
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
You may optionally set the host and port used by the `pdoc` web-server using the `-p` and `-h` options.
|
|
109
|
+
See `pdoc --help` for more info.
|