Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

python(feat): Report templates service #145

Merged
merged 32 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c09c81e
feat: Initial ReportTemplateService and ReportTemplateConfig
niliayu Oct 25, 2024
df27432
feat: Add create & update methods to service
niliayu Oct 26, 2024
d967468
test: ReportTemplateConfig init and start adding service tests
niliayu Nov 5, 2024
3d2591d
chore: import order fix
niliayu Nov 5, 2024
f2e9083
test: Add service tests
niliayu Nov 6, 2024
d11cf48
chore: Added get_report_template()
niliayu Nov 12, 2024
77dc911
chore: dev check
niliayu Nov 12, 2024
1d805ad
chore: Switch ReportTemplateConfig to pydantic model
niliayu Nov 13, 2024
0594e5b
test: ReportTemplateConfig unit tests
niliayu Nov 13, 2024
b6e66ae
feat: Add report template initial example
niliayu Nov 16, 2024
48a9d66
fix: simplify & fix example
niliayu Nov 19, 2024
a220916
placeholder
niliayu Nov 19, 2024
112a239
feat: Clean up update report template function
niliayu Nov 21, 2024
bf7b84e
chore: clean up
niliayu Nov 22, 2024
f037452
chore: placeholder, starting yaml setup
niliayu Nov 22, 2024
1951808
refactor: Just use rule client keys
niliayu Nov 23, 2024
aee8efb
feat: Add back get function and fix unit tests
niliayu Nov 23, 2024
7851c6c
refactor: use client keys instead of full rules in example
niliayu Nov 23, 2024
d240dfe
refactor: use client keys instead of full rules in example
niliayu Nov 23, 2024
4d49593
chore: dev check
niliayu Nov 23, 2024
aee589b
placeholder for yaml
niliayu Nov 24, 2024
72409c5
feat: Add YAML support and examples
niliayu Nov 26, 2024
bf4fa05
chore: Cleanups
niliayu Nov 26, 2024
1c2c81f
chore: lint
niliayu Nov 26, 2024
b26756a
test: Fix config refactor
niliayu Nov 26, 2024
f273065
feat: Add archived_date
niliayu Nov 26, 2024
739478e
chore: fmt
niliayu Nov 26, 2024
a8a060e
fix: Include archived_date when fetching report templates
niliayu Nov 26, 2024
570db75
doc: Report template yaml spec
niliayu Nov 27, 2024
16cbd1b
chore: fmt
niliayu Nov 27, 2024
2820bec
chore: PR feedback
niliayu Dec 2, 2024
7f860d8
chore: Move common yaml util
niliayu Dec 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Retrieve the BASE_URI from the Sift team
# Be sure to exclude "https://" from the BASE_URI
#
# BASE URIs and further details can be found here:
# https://docs.siftstack.com/ingestion/overview
kevin-sift marked this conversation as resolved.
Show resolved Hide resolved
BASE_URI=""

SIFT_API_KEY=""
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
kinetic_energy_gt:
0.5 * $mass * $1 * $1 > $threshold
rod_torque_gt:
(1 / 12) * $mass * $rod_length * $rod_length * $1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
log_substring_contains:
contains($1, $sub_string)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import os

from dotenv import load_dotenv
from report_template_config import load_rules, nostromos_report_template
from sift_py.grpc.transport import SiftChannelConfig, use_sift_channel
from sift_py.report_templates.service import ReportTemplateService
from sift_py.rule.service import RuleService

if __name__ == "__main__":
load_dotenv()

apikey = os.getenv("SIFT_API_KEY")
assert apikey, "Missing 'SIFT_API_KEY' environment variable."

base_uri = os.getenv("BASE_URI")
assert base_uri, "Missing 'BASE_URI' environment variable."

# Create a gRPC transport channel for the Sift API
sift_channel_config = SiftChannelConfig(uri=base_uri, apikey=apikey)

with use_sift_channel(sift_channel_config) as channel:
# First create rules
rule_service = RuleService(channel)
rules = load_rules() # Load rules from python
[rule_service.create_or_update_rule(rule) for rule in rules]

# Now create report template
report_template_service = ReportTemplateService(channel)
report_template = nostromos_report_template()
report_template.rule_client_keys = [
rule.rule_client_key for rule in rules if rule.rule_client_key
] # Add the rules we just created
report_template_service.create_or_update_report_template(report_template)

# Then make some updates to the template we created (for the sake of example)
rules = [rule for rule in rules if "overheating" not in rule.name] # Remove some rules
# Get the report template (for the sake of example)
report_template_to_update = report_template_service.get_report_template(
client_key=report_template.template_client_key
)
if report_template_to_update: # Make some other changes
report_template_to_update.rule_client_keys = [
rule.rule_client_key for rule in rules if rule.rule_client_key
]
report_template_to_update.description = (
"A report template for the Nostromo without overheating rule"
)
report_template_service.create_or_update_report_template(report_template_to_update)
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from pathlib import Path
from typing import List

from sift_py.ingestion.channel import (
ChannelConfig,
ChannelDataType,
ChannelEnumType,
)
from sift_py.ingestion.config.yaml.load import load_named_expression_modules
from sift_py.ingestion.rule.config import (
RuleActionCreateDataReviewAnnotation,
RuleConfig,
)
from sift_py.report_templates.config import ReportTemplateConfig

EXPRESSION_MODULES_DIR = Path().joinpath("expression_modules")


def load_rules() -> List[RuleConfig]:
named_expressions = load_named_expression_modules(
[
EXPRESSION_MODULES_DIR.joinpath("kinematics.yml"),
EXPRESSION_MODULES_DIR.joinpath("string.yml"),
]
)

log_channel = ChannelConfig(
name="log",
data_type=ChannelDataType.STRING,
description="asset logs",
)
voltage_channel = ChannelConfig(
name="voltage",
data_type=ChannelDataType.INT_32,
description="voltage at source",
unit="Volts",
)
vehicle_state_channel = ChannelConfig(
name="vehicle_state",
data_type=ChannelDataType.ENUM,
description="vehicle state",
enum_types=[
ChannelEnumType(name="Accelerating", key=0),
ChannelEnumType(name="Decelerating", key=1),
ChannelEnumType(name="Stopped", key=2),
],
)

rules = [
RuleConfig(
name="overheating",
description="Checks for vehicle overheating",
expression='$1 == "Accelerating" && $2 > 80',
rule_client_key="overheating-rule",
asset_names=["NostromoLV2024"],
channel_references=[
# INFO: Can use either "channel_identifier" or "channel_config"
{
"channel_reference": "$1",
"channel_identifier": vehicle_state_channel.fqn(),
},
{
"channel_reference": "$2",
"channel_config": voltage_channel,
},
],
action=RuleActionCreateDataReviewAnnotation(),
),
RuleConfig(
name="kinetic_energy",
description="Tracks high energy output while in motion",
expression=named_expressions["kinetic_energy_gt"],
rule_client_key="kinetic-energy-rule",
asset_names=["NostromoLV2024"],
channel_references=[
{
"channel_reference": "$1",
"channel_config": voltage_channel,
},
],
sub_expressions={
"$mass": 10,
"$threshold": 470,
},
action=RuleActionCreateDataReviewAnnotation(
tags=["nostromo"],
),
),
RuleConfig(
name="failure",
description="Checks for failures reported by logs",
expression=named_expressions["log_substring_contains"],
rule_client_key="failure-rule",
asset_names=["NostromoLV2024"],
channel_references=[
{
"channel_reference": "$1",
"channel_config": log_channel,
},
],
sub_expressions={
"$sub_string": "failure",
},
action=RuleActionCreateDataReviewAnnotation(
tags=["nostromo", "failure"],
),
),
]
return rules


def nostromos_report_template() -> ReportTemplateConfig:
return ReportTemplateConfig(
name="Nostromo Report Template",
template_client_key="nostromo-report-template-test",
description="A report template for the Nostromo",
rule_client_keys=[],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
python-dotenv
sift-stack-py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Retrieve the BASE_URI from the Sift team
# Be sure to exclude "https://" from the BASE_URI
#
# BASE URIs and further details can be found here:
# https://docs.siftstack.com/ingestion/overview
BASE_URI=""

SIFT_API_KEY=""
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
kinetic_energy_gt:
0.5 * $mass * $1 * $1 > $threshold
rod_torque_gt:
(1 / 12) * $mass * $rod_length * $rod_length * $1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
log_substring_contains:
contains($1, $sub_string)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import os
from datetime import datetime
from pathlib import Path

from dotenv import load_dotenv
from sift_py.grpc.transport import SiftChannelConfig, use_sift_channel
from sift_py.ingestion.config.yaml.load import load_named_expression_modules
from sift_py.report_templates.service import ReportTemplateService
from sift_py.rule.service import RuleService, SubExpression

REPORT_TEMPLATES_DIR = Path().joinpath("report_templates")
RULE_MODULES_DIR = Path().joinpath("rule_modules")
EXPRESSION_MODULES_DIR = Path().joinpath("expression_modules")

if __name__ == "__main__":
load_dotenv()

apikey = os.getenv("SIFT_API_KEY")
assert apikey, "Missing 'SIFT_API_KEY' environment variable."

base_uri = os.getenv("BASE_URI")
assert base_uri, "Missing 'BASE_URI' environment variable."

# Create a gRPC transport channel configured specifically for the Sift API
sift_channel_config = SiftChannelConfig(uri=base_uri, apikey=apikey)

# Paths to your rules, named expressions, and report template
report_templates = REPORT_TEMPLATES_DIR.joinpath("nostromo_report_template.yml")
rule_modules = RULE_MODULES_DIR.joinpath("rules.yml")
named_expressions = load_named_expression_modules(
[
EXPRESSION_MODULES_DIR.joinpath("kinematics.yml"),
EXPRESSION_MODULES_DIR.joinpath("string.yml"),
]
)

with use_sift_channel(sift_channel_config) as channel:
# First create rules
rule_service = RuleService(channel)
rules = rule_service.load_rules_from_yaml(
paths=[rule_modules],
sub_expressions=[
SubExpression("kinetic_energy", named_expressions),
SubExpression("failure", named_expressions),
],
)

# Now create report templates
report_template_service = ReportTemplateService(channel)
report_template_service.load_report_templates_from_yaml([report_templates])

# Archive one template, for the sake of example
report_template_to_update = report_template_service.get_report_template(
client_key="nostromo-report-template-1"
)
if report_template_to_update:
report_template_to_update.archived_date = datetime.now()
report_template_service.create_or_update_report_template(report_template_to_update)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
report_templates:
- name: Nostromo Report Template 1
description: A report tempate for the Nostromo
template_client_key: nostromo-report-template-1
rule_client_keys:
- kinetic-energy-rule
- failure-rule

- name: Nostromo Report Template Overheating
description: A report tempate for the Nostromo
template_client_key: nostromo-report-template-overheating
rule_client_keys:
- overheating-rule
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
python-dotenv
sift-stack-py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace: nostromo

rules:
- name: overheating
description: Checks for vehicle overheating
expression: $1 == "Accelerating" && $2 > 80
rule_client_key: overheating-rule
channel_references:
- $1: *vehicle_state_channel
- $2: *voltage_channel
type: review

- name: kinetic_energy
description: Tracks high energy output while in motion
type: review
expression:
name: kinetic_energy_gt
rule_client_key: kinetic-energy-rule
channel_references:
- $1: *velocity_channel
sub_expressions:
- $mass: 10
- $threshold: 470
tags:
- nostromo

- name: failure
description: Checks for failures reported by logs
type: review
rule_client_key: failure-rule
expression:
name: log_substring_contains
channel_references:
- $1: *log_channel
sub_expressions:
- $sub_string: failure
tags:
- failure
- nostromo

13 changes: 3 additions & 10 deletions python/lib/sift_py/ingestion/config/yaml/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
TelemetryConfigYamlSpec,
)
from sift_py.ingestion.rule.config import RuleActionAnnotationKind
from sift_py.yaml.utils import _handle_subdir

_CHANNEL_REFERENCE_REGEX = re.compile(r"^\$\d+$")
_SUB_EXPRESSION_REGEX = re.compile(r"^\$[a-zA-Z_]+$")
Expand Down Expand Up @@ -70,19 +71,11 @@ def update_rule_namespaces(rule_module_path: Path):
raise YamlConfigError(
f"Encountered rules with identical names being loaded, '{key}'."
)

rule_namespaces.update(rule_module)

def handle_dir(path: Path):
for file_in_dir in path.iterdir():
if file_in_dir.is_dir():
handle_dir(file_in_dir)
elif file_in_dir.is_file():
update_rule_namespaces(file_in_dir)

for path in paths:
if path.is_dir():
handle_dir(path)
_handle_subdir(path, update_rule_namespaces)
elif path.is_file():
update_rule_namespaces(path)

Expand Down Expand Up @@ -118,7 +111,7 @@ def _read_rule_namespace_yaml(path: Path) -> Dict[str, List]:
)

rules = namespace_rules.get("rules")
if not isinstance(namespace, str):
if not isinstance(rules, list):
raise YamlConfigError(
f"Expected '{rules}' to be a list in rule namespace yaml: '{path}'"
f"{_type_fqn(RuleNamespaceYamlSpec)}"
Expand Down
4 changes: 2 additions & 2 deletions python/lib/sift_py/ingestion/config/yaml/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from __future__ import annotations

from typing import Dict, List, Literal, TypedDict, Union
from typing import Dict, List, Literal, Union

from typing_extensions import NotRequired
from typing_extensions import NotRequired, TypedDict


class TelemetryConfigYamlSpec(TypedDict):
Expand Down
Empty file.
Loading
Loading