diff --git a/pyproject.toml b/pyproject.toml index 29068789..aa43c38b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "splight-lib" -version = "5.24.13" +version = "5.25.0" description = "Splight Python Library" authors = [ {name = "Splight Dev",email = "dev@splight.com"} diff --git a/splight_lib/client/database/classmap.py b/splight_lib/client/database/classmap.py index acc003a2..29a5b4db 100644 --- a/splight_lib/client/database/classmap.py +++ b/splight_lib/client/database/classmap.py @@ -35,7 +35,6 @@ "file": f"{ENGINE_PREFIX}/file/files/", "filter": f"{ENGINE_PREFIX}/dashboard/filters/", "folder": f"{ENGINE_PREFIX}/file/folders/", - "function": f"{ENGINE_PREFIX}/function/functions/", "generator": f"{ENGINE_PREFIX}/asset/generators/", "grid": f"{ENGINE_PREFIX}/asset/grids/", "inverter": f"{ENGINE_PREFIX}/asset/inverters/", diff --git a/splight_lib/models/__init__.py b/splight_lib/models/__init__.py index e613e19e..0cd7d0e3 100644 --- a/splight_lib/models/__init__.py +++ b/splight_lib/models/__init__.py @@ -48,8 +48,6 @@ File, Filter, Folder, - Function, - FunctionItem, Generator, Grid, HubComponent, @@ -63,7 +61,6 @@ Parameter, PipelineStep, PrivacyPolicy, - QueryFilter, Routine, RoutineObject, RoutineObjectInstance, @@ -130,10 +127,7 @@ "File", "Filter", "Folder", - "Function", - "FunctionItem", "HubComponent", - "QueryFilter", "Metadata", "Number", "PipelineStep", diff --git a/splight_lib/models/_v3/__init__.py b/splight_lib/models/_v3/__init__.py index 787d2dc4..c143ea54 100644 --- a/splight_lib/models/_v3/__init__.py +++ b/splight_lib/models/_v3/__init__.py @@ -62,7 +62,6 @@ from splight_lib.models._v3.external_grid import ExternalGrid from splight_lib.models._v3.file import File from splight_lib.models._v3.folder import Folder -from splight_lib.models._v3.function import Function, FunctionItem, QueryFilter from splight_lib.models._v3.generator import Generator from splight_lib.models._v3.grid import Grid from splight_lib.models._v3.hub import HubComponent @@ -129,10 +128,7 @@ "File", "Filter", "Folder", - "Function", - "FunctionItem", "HubComponent", - "QueryFilter", "Metadata", "Number", "PipelineStep", diff --git a/splight_lib/models/_v3/exceptions.py b/splight_lib/models/_v3/exceptions.py index dc4ab4b9..7a73b7f0 100644 --- a/splight_lib/models/_v3/exceptions.py +++ b/splight_lib/models/_v3/exceptions.py @@ -18,14 +18,6 @@ def __init__(self, name: str): super().__init__(msg) -class InvalidFunctionConfiguration(Exception): - pass - - -class MissingFunctionItemExpression(Exception): - pass - - class InvalidAlertConfiguration(Exception): pass diff --git a/splight_lib/models/_v3/function.py b/splight_lib/models/_v3/function.py deleted file mode 100644 index cb13367e..00000000 --- a/splight_lib/models/_v3/function.py +++ /dev/null @@ -1,221 +0,0 @@ -import json -import re -from enum import auto -from typing import Annotated, Literal - -from pydantic import ( - BaseModel, - Field, - PositiveInt, - ValidationError, - model_validator, -) -from strenum import LowercaseStrEnum, UppercaseStrEnum -from typing_extensions import TypedDict - -from splight_lib.models._v3.exceptions import InvalidFunctionConfiguration -from splight_lib.models._v3.generic import ValueTypeEnum -from splight_lib.models.database import ( - ResourceSummary, - SplightDatabaseBaseModel, -) - - -class FunctionItemType(UppercaseStrEnum): - EXPRESSION = auto() - QUERY = auto() - METADATA = auto() - - -class QueryFilter(TypedDict): - id: str - name: str - - -class TypedQueryFilter(TypedDict): - id: str - name: str - type: ValueTypeEnum - - -class GroupUnit(LowercaseStrEnum): - EMPTY = "" - SECOND = auto() - MINUTE = auto() - HOUR = auto() - DAY = auto() - MONTH = auto() - - -class GroupCriteria(LowercaseStrEnum): - EMPTY = "" - AVG = auto() - SUM = auto() - MIN = auto() - MAX = auto() - LAST = auto() - - -class FunctionItem(BaseModel): - # NOTE: the defaults are inconsistent because - # we never made a clear contract between the lib, api and frontend - - id: Annotated[str | None, Field(None, max_length=100)] - - ref_id: Annotated[str, Field(max_length=5)] - type: FunctionItemType = FunctionItemType.QUERY - - expression: str | None = "" - expression_plain: str | None = "" - - query_filter_asset: QueryFilter | None = None - query_filter_attribute: TypedQueryFilter | None = None - - query_group_function: GroupCriteria | None = GroupCriteria.EMPTY - query_group_unit: GroupUnit | None = GroupUnit.EMPTY - - query_sort_field: str | None = "" - query_sort_direction: Annotated[int, Field(-1, ge=-1, le=1)] - - query_plain: str | None = "" - - @model_validator(mode="after") - def validate_expression(self): - if self.type == FunctionItemType.EXPRESSION: - self.expression_plain = ( - self._get_expression_plain() - if self.expression_plain is None - else self.expression_plain - ) - return self - - @model_validator(mode="after") - def validate_query(self): - if self.type == FunctionItemType.QUERY: - for attr in [ - ("query_filter_asset", self.query_filter_asset), - ("query_filter_attribute", self.query_filter_attribute), - ]: - if attr is None: - raise ValidationError( - ( - f"Parameter '{attr}' is required for query type " - "functions items" - ) - ) - self.query_plain = ( - self._get_query_plain() - if self.query_plain is None - else self.query_plain - ) - return self - - def _get_expression_plain(self) -> str | None: - if self.expression is None: - return None - pattern = r"\$\w+" - args = re.findall(pattern, self.expression) - - str_args = ", ".join(args) - body = f"function ({str_args}) {{ return {self.expression} }}" - expression_plain = { - "$function": {"body": body, "args": args, "lang": "js"} - } - return json.dumps(expression_plain) - - def _get_query_plain(self): - query_plain = [ - { - "$match": { - "asset": self.query_filter_asset["id"], - "attribute": self.query_filter_attribute["id"], - } - }, - ] - # NOTE: The frontend only sets the rest of the query - # if both group_unit and group_function are set - if self.query_group_unit and self.query_group_function: - query_plain.extend( - [ - { - "$addFields": { - "timestamp": { - "$dateTrunc": { - "date": "$timestamp", - "unit": self.query_group_unit, - "binSize": 1, - } - } - } - }, - { - "$group": { - "_id": "$timestamp", - "value": { - f"${self.query_group_function}": "$value" - }, - "timestamp": {"$last": "$timestamp"}, - } - }, - ] - ) - return json.dumps(query_plain) - - -class Function(SplightDatabaseBaseModel): - id: Annotated[str | None, Field(None, max_length=100)] - name: str - description: str | None = None - - tags: list[ResourceSummary] | None = None - active: bool = True - time_window: int = 5 * 60 - function_items: list[FunctionItem] = [] - - type: Literal["cron", "rate"] - target_variable: str - target_asset: QueryFilter # NOTE: optional in API - target_attribute: TypedQueryFilter # NOTE: optional in API - - cron_minutes: str | None = None - cron_hours: str | None = None - cron_dom: str | None = None - cron_month: str | None = None - cron_dow: str | None = None - cron_year: str | None = None - - rate_value: PositiveInt | None = None - rate_unit: Literal["day", "hour", "minute"] | None = None - - @model_validator(mode="after") - def validate_type(self): - if self.type == "cron": - for attr, value in [ - ("cron_year", self.cron_year), - ("cron_moth", self.cron_month), - ("cron_hour", self.cron_hours), - ("cron_minutes", self.cron_minutes), - ("cron_dow", self.cron_dow), - ("cron_dom", self.cron_dom), - ]: - if value is None: - raise InvalidFunctionConfiguration( - ( - f"Parameter {attr} is required for '{self.type}' " - "type functions" - ) - ) - if self.type == "rate": - for attr in [ - ("rate_value", self.rate_value), - ("rate_unit", self.rate_unit), - ]: - if attr is None: - raise InvalidFunctionConfiguration( - ( - f"Parameter {attr} is required for '{self.type}' " - "type functions" - ) - ) - - return self diff --git a/splight_lib/tests/test_api_contracts.py b/splight_lib/tests/test_api_contracts.py index be4001cc..d83b3c7e 100644 --- a/splight_lib/tests/test_api_contracts.py +++ b/splight_lib/tests/test_api_contracts.py @@ -15,14 +15,12 @@ from splight_lib.models import ( Asset, Attribute, - Function, Metadata, RoutineObject, Secret, ) from splight_lib.models._v3.component import InputDataAddress, InputParameter from splight_lib.models._v3.data_address import DataAddresses -from splight_lib.models._v3.function import FunctionItem ASSET_METADATA_SET = [ Metadata( @@ -84,33 +82,6 @@ class AssetFactory(ModelFactory[Asset]): timezone = "America/Los_Angeles" -class FunctionItemFactory(ModelFactory[FunctionItem]): - id = Use(lambda: str(uuid.uuid4())) - query_filter_asset = Use( - lambda: {"id": str(uuid.uuid4()), "name": "asset"} - ) - query_filter_attribute = Use( - lambda: { - "id": str(uuid.uuid4()), - "name": "attribute", - "type": "Number", - } - ) - - -class FunctionFactory(ModelFactory[Function]): - __allow_none_optionals__ = False - - id: str = Use(lambda: str(uuid.uuid4())) - type = "cron" - - target_variable = "Z" - target_asset = AssetFactory.build().model_dump() - target_attribute = AttributeFactory.build().model_dump() - - function_items = FunctionItemFactory.batch(random.randint(1, 10)) - - class DataAddressesFactory(ModelFactory[DataAddresses]): asset = Use(lambda: str(uuid.uuid4())) attribute = Use(lambda: str(uuid.uuid4())) @@ -138,7 +109,6 @@ class SecretFactory(ModelFactory[Secret]): MODEL_MAPPING = { "Attribute": ModelMapping(AttributeFactory, "AssetAttribute"), "Asset": ModelMapping(AssetFactory, "Asset"), - "Function": ModelMapping(FunctionFactory, "Function"), "RoutineObject": ModelMapping(RoutineObjectFactory, "RoutineObject"), "Secret": ModelMapping(SecretFactory, "Secret"), # TODO: Include File and Component