diff --git a/python/lib/sift_client/.ruff.toml b/python/lib/sift_client/.ruff.toml new file mode 100644 index 000000000..e5e6a152f --- /dev/null +++ b/python/lib/sift_client/.ruff.toml @@ -0,0 +1,60 @@ +extend = "../../pyproject.toml" + +[lint] +select = [ + # Core linting + "F", # pyflakes - detect various errors + "W", # pycodestyle warnings - style recommendations + "B", # flake8-bugbear - detect potential bugs and design problems + "PERF", # perflint - performance optimizations + "RUF", # ruff-specific rules + # "ASYNC", # flake8-async - async/await best practices # TODO + + # Code style and formatting + "I", # isort - import sorting + "N", # pep8-naming - naming conventions + "C4", # flake8-comprehensions - better list/dict/set comprehensions + "UP", # pyupgrade - modernize syntax to latest Python + + # Documentation + "D", # pydocstyle - docstring style checking + + # Imports and modules + "TID", # flake8-tidy-imports - import organization + "INP", # flake8-no-pep420 - no implicit namespace packages + + # Type checking and annotations + "FA", # flake8-future-annotations - necessary for backwards compatibility + "TC", # flake8-type-checking - type checking best practices + + # Built-ins and standard library + "A", # flake8-builtins - prevent overriding built-ins + "DTZ", # flake8-datetimez - good timezone practices + + # Exception handling + # "TRY", # tryceratops - exception handling antipatterns # TODO: FD-102 + + # Logging best practices + # "G", # flake8-logging-format - logging format best practices # TODO: FD-101 + "LOG", # flake8-logging - logging best practices + + # Tests + # "PT", # flake8-pytest-style - pytest best practices # TODO: FD-59 + +] + +ignore = ["W191", "D206", "D300", # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "B024", # ignore missing abstract methods + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D205", # 1 blank line required between summary line and description + "D100", # Missing docstring in public module +] + + +[lint.pydocstyle] +convention = "google" + +[lint.per-file-ignores] +"examples/*" = ["D"] +"_internal/*" = ["D"] # Private docs, be less strict \ No newline at end of file diff --git a/python/lib/sift_client/README.md b/python/lib/sift_client/README.md index f322b6ab5..a1b336700 100644 --- a/python/lib/sift_client/README.md +++ b/python/lib/sift_client/README.md @@ -130,7 +130,7 @@ asset.update({ For more complex updates, you can create update models (instead of a key-value dictionary): ```python -from sift_client.types.asset import AssetUpdate +from sift_client.sift_types.asset import AssetUpdate # Create an update model update = AssetUpdate(tags=["new", "tags"]) diff --git a/python/lib/sift_client/__init__.py b/python/lib/sift_client/__init__.py index 2ced0916c..49bdc04d9 100644 --- a/python/lib/sift_client/__init__.py +++ b/python/lib/sift_client/__init__.py @@ -1,5 +1,4 @@ -""" -!!! warning +"""!!! warning The Sift Client is experimental and is subject to change. diff --git a/python/lib/sift_client/_internal/gen_pyi.py b/python/lib/sift_client/_internal/gen_pyi.py index 3b9e871dc..e798f997f 100644 --- a/python/lib/sift_client/_internal/gen_pyi.py +++ b/python/lib/sift_client/_internal/gen_pyi.py @@ -23,20 +23,16 @@ class {cls_name}: {methods} """ -METHOD_TEMPLATE = '''\ +METHOD_TEMPLATE = """\ {decorator} def {meth_name}(self{params}){ret_ann}: - """ - {meth_doc} - """ +{docstring_section} ... -''' +""" def extract_imports(path: pathlib.Path) -> list[str]: - """ - Parse the given Python file and return a list of its import statements (as strings). - """ + """Parse the given Python file and return a list of its import statements (as strings).""" source = path.read_text() tree = ast.parse(source, filename=str(path)) @@ -125,7 +121,8 @@ def generate_stubs_for_module(path_arg: str | pathlib.Path) -> dict[pathlib.Path async_class = matched.get("async_cls") if async_class is None: warnings.warn( - f"Could not find async class for {cls_name}. Skipping stub generation." + f"Could not find async class for {cls_name}. Skipping stub generation.", + stacklevel=2, ) continue @@ -133,14 +130,14 @@ def generate_stubs_for_module(path_arg: str | pathlib.Path) -> dict[pathlib.Path source_file = inspect.getsourcefile(async_class) if source_file is None: warnings.warn( - f"Could not find source file for {async_class.__name__}. Skipping stub generation." + f"Could not find source file for {async_class.__name__}. Skipping stub generation.", + stacklevel=2, ) continue orig_path = pathlib.Path(source_file).resolve() imports = extract_imports(orig_path) - for imp in imports: - new_module_imports.append(imp) + new_module_imports = new_module_imports + imports # Class docstring raw_doc = inspect.getdoc(cls) or "" @@ -183,7 +180,7 @@ def generate_stubs_for_module(path_arg: str | pathlib.Path) -> dict[pathlib.Path lines.append(stub) unique_imports = list(OrderedDict.fromkeys(new_module_imports)) - lines = [HEADER] + unique_imports + lines + lines = [HEADER, *unique_imports, *lines] pyi_file = py_file.with_suffix(".pyi") stub_files[pyi_file] = "\n".join(lines) @@ -264,14 +261,18 @@ def generate_method_stub(name: str, f: Callable, module, decorator: str = "") -> # Method docstring raw_mdoc = inspect.getdoc(f) or "" - meth_doc = raw_mdoc.replace('"""', '\\"\\"\\"').replace("\n", "\n ") + if raw_mdoc and raw_mdoc.strip(): + meth_doc = raw_mdoc.replace('"""', '\\"\\"\\"').replace("\n", "\n ") + docstring_section = f' """\n {meth_doc}\n """\n' + else: + docstring_section = "" return METHOD_TEMPLATE.format( decorator=decorator, meth_name=name, params=params_txt, ret_ann=ret_txt, - meth_doc=meth_doc, + docstring_section=docstring_section, ) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/assets.py b/python/lib/sift_client/_internal/low_level_wrappers/assets.py index 4dd38aecc..c6c2fdb86 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/assets.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/assets.py @@ -17,23 +17,21 @@ from sift_client._internal.low_level_wrappers.base import ( LowLevelClientBase, ) +from sift_client.sift_types.asset import Asset, AssetUpdate from sift_client.transport import GrpcClient, WithGrpcClient -from sift_client.types.asset import Asset, AssetUpdate # Configure logging logger = logging.getLogger(__name__) class AssetsLowLevelClient(LowLevelClientBase, WithGrpcClient): - """ - Low-level client for the AssetsAPI. + """Low-level client for the AssetsAPI. This class provides a thin wrapper around the autogenerated bindings for the AssetsAPI. """ def __init__(self, grpc_client: GrpcClient): - """ - Initialize the AssetsLowLevelClient. + """Initialize the AssetsLowLevelClient. Args: grpc_client: The gRPC client to use for making API calls. @@ -43,7 +41,7 @@ def __init__(self, grpc_client: GrpcClient): async def get_asset(self, asset_id: str) -> Asset: request = GetAssetRequest(asset_id=asset_id) response = await self._grpc_client.get_stub(AssetServiceStub).GetAsset(request) - grpc_asset = cast(GetAssetResponse, response).asset + grpc_asset = cast("GetAssetResponse", response).asset return Asset._from_proto(grpc_asset) async def list_all_assets( @@ -53,8 +51,7 @@ async def list_all_assets( max_results: int | None = None, page_size: int | None = None, ) -> list[Asset]: - """ - List all results matching the given query. + """List all results matching the given query. Args: query_filter: The CEL query filter. @@ -93,14 +90,14 @@ async def list_assets( request = ListAssetsRequest(**request_kwargs) response = await self._grpc_client.get_stub(AssetServiceStub).ListAssets(request) - response = cast(ListAssetsResponse, response) + response = cast("ListAssetsResponse", response) return [Asset._from_proto(asset) for asset in response.assets], response.next_page_token async def update_asset(self, update: AssetUpdate) -> Asset: grpc_asset, update_mask = update.to_proto_with_mask() request = UpdateAssetRequest(asset=grpc_asset, update_mask=update_mask) response = await self._grpc_client.get_stub(AssetServiceStub).UpdateAsset(request) - updated_grpc_asset = cast(UpdateAssetResponse, response).asset + updated_grpc_asset = cast("UpdateAssetResponse", response).asset return Asset._from_proto(updated_grpc_asset) async def delete_asset(self, asset_id: str, archive_runs: bool = False) -> None: diff --git a/python/lib/sift_client/_internal/low_level_wrappers/base.py b/python/lib/sift_client/_internal/low_level_wrappers/base.py index 5009a0bd9..a8b93ed68 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/base.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/base.py @@ -8,14 +8,13 @@ class LowLevelClientBase(ABC): @staticmethod async def _handle_pagination( func: Callable, - kwargs: dict[str, Any] = {}, + kwargs: dict[str, Any] | None = None, page_size: int | None = None, page_token: str | None = None, order_by: str | None = None, max_results: int | None = None, ) -> list[Any]: - """ - Handle pagination for a given function by calling the function until all results are retrieved or the max_results is reached. + """Handle pagination for a given function by calling the function until all results are retrieved or the max_results is reached. Args: func: The function to call. @@ -28,6 +27,9 @@ async def _handle_pagination( Returns: A list of all matching results. """ + if kwargs is None: + kwargs = {} + results: list[Any] = [] if page_token is None: page_token = "" diff --git a/python/lib/sift_client/_internal/low_level_wrappers/calculated_channels.py b/python/lib/sift_client/_internal/low_level_wrappers/calculated_channels.py index 6ca764b37..70af5f93e 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/calculated_channels.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/calculated_channels.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import Any, List, cast +from typing import TYPE_CHECKING, Any, cast from sift.calculated_channels.v2.calculated_channels_pb2 import ( CalculatedChannelAbstractChannelReference, @@ -23,26 +23,26 @@ from sift.calculated_channels.v2.calculated_channels_pb2_grpc import CalculatedChannelServiceStub from sift_client._internal.low_level_wrappers.base import LowLevelClientBase -from sift_client.transport import GrpcClient, WithGrpcClient -from sift_client.types.calculated_channel import ( +from sift_client.sift_types.calculated_channel import ( CalculatedChannel, CalculatedChannelUpdate, ) -from sift_client.types.channel import ChannelReference +from sift_client.transport import GrpcClient, WithGrpcClient + +if TYPE_CHECKING: + from sift_client.sift_types.channel import ChannelReference logger = logging.getLogger(__name__) class CalculatedChannelsLowLevelClient(LowLevelClientBase, WithGrpcClient): - """ - Low-level client for the CalculatedChannelsAPI. + """Low-level client for the CalculatedChannelsAPI. This class provides a thin wrapper around the autogenerated bindings for the CalculatedChannelsAPI. """ def __init__(self, grpc_client: GrpcClient): - """ - Initialize the CalculatedChannelsLowLevelClient. + """Initialize the CalculatedChannelsLowLevelClient. Args: grpc_client: The gRPC client to use for making API calls. @@ -56,8 +56,7 @@ async def get_calculated_channel( client_key: str | None = None, organization_id: str | None = None, ) -> CalculatedChannel: - """ - Get a calculated channel by ID or client key. + """Get a calculated channel by ID or client key. Args: calculated_channel_id: The ID of the calculated channel. @@ -76,7 +75,7 @@ async def get_calculated_channel( response = await self._grpc_client.get_stub( CalculatedChannelServiceStub ).GetCalculatedChannel(request) - grpc_calculated_channel = cast(GetCalculatedChannelResponse, response).calculated_channel + grpc_calculated_channel = cast("GetCalculatedChannelResponse", response).calculated_channel return CalculatedChannel._from_proto(grpc_calculated_channel) async def create_calculated_channel( @@ -87,14 +86,13 @@ async def create_calculated_channel( asset_ids: list[str] | None = None, tag_ids: list[str] | None = None, expression: str = "", - channel_references: list[ChannelReference] = [], + channel_references: list[ChannelReference] | None = None, description: str = "", user_notes: str = "", units: str | None = None, client_key: str | None = None, ) -> tuple[CalculatedChannel, list[Any]]: - """ - Create a calculated channel. + """Create a calculated channel. Args: name: The name of the calculated channel. @@ -111,6 +109,9 @@ async def create_calculated_channel( Returns: A tuple of (CalculatedChannel, list of inapplicable assets). """ + if channel_references is None: + channel_references = [] + asset_config = CalculatedChannelAssetConfiguration( all_assets=all_assets, selection=CalculatedChannelAssetConfiguration.AssetSelection( @@ -147,7 +148,7 @@ async def create_calculated_channel( response = await self._grpc_client.get_stub( CalculatedChannelServiceStub ).CreateCalculatedChannel(request) - response = cast(CreateCalculatedChannelResponse, response) + response = cast("CreateCalculatedChannelResponse", response) calculated_channel = CalculatedChannel._from_proto(response.calculated_channel) inapplicable_assets = list(response.inapplicable_assets) @@ -163,8 +164,7 @@ async def list_all_calculated_channels( page_size: int | None = None, organization_id: str | None = None, ) -> list[CalculatedChannel]: - """ - List all calculated channels matching the given query. + """List all calculated channels matching the given query. Args: query_filter: The CEL query filter. @@ -193,8 +193,7 @@ async def list_calculated_channels( order_by: str | None = None, organization_id: str | None = None, ) -> tuple[list[CalculatedChannel], str]: - """ - List calculated channels with pagination. + """List calculated channels with pagination. Args: page_size: The number of results to return per page. @@ -222,7 +221,7 @@ async def list_calculated_channels( response = await self._grpc_client.get_stub( CalculatedChannelServiceStub ).ListCalculatedChannels(request) - response = cast(ListCalculatedChannelsResponse, response) + response = cast("ListCalculatedChannelsResponse", response) calculated_channels = [ CalculatedChannel._from_proto(cc) for cc in response.calculated_channels @@ -234,9 +233,8 @@ async def update_calculated_channel( *, update: CalculatedChannelUpdate, user_notes: str | None = None, - ) -> tuple[CalculatedChannel, List[Any]]: - """ - Update a calculated channel. + ) -> tuple[CalculatedChannel, list[Any]]: + """Update a calculated channel. Args: update: The CalculatedChannelUpdate to apply. @@ -254,11 +252,12 @@ async def update_calculated_channel( response = await self._grpc_client.get_stub( CalculatedChannelServiceStub ).UpdateCalculatedChannel(request) - response = cast(UpdateCalculatedChannelResponse, response) + response = cast("UpdateCalculatedChannelResponse", response) updated_calculated_channel = CalculatedChannel._from_proto(response.calculated_channel) inapplicable_assets = [ - cast(CalculatedChannelValidationResult, asset) for asset in response.inapplicable_assets + cast("CalculatedChannelValidationResult", asset) + for asset in response.inapplicable_assets ] return updated_calculated_channel, inapplicable_assets @@ -274,8 +273,7 @@ async def list_calculated_channel_versions( query_filter: str | None = None, order_by: str | None = None, ) -> tuple[list[CalculatedChannel], str]: - """ - List versions of a calculated channel. + """List versions of a calculated channel. Args: calculated_channel_id: The ID of the calculated channel. @@ -315,7 +313,7 @@ async def list_calculated_channel_versions( response = await self._grpc_client.get_stub( CalculatedChannelServiceStub ).ListCalculatedChannelVersions(request) - response = cast(ListCalculatedChannelVersionsResponse, response) + response = cast("ListCalculatedChannelVersionsResponse", response) versions = [ CalculatedChannel._from_proto(cc) for cc in response.calculated_channel_versions @@ -332,9 +330,7 @@ async def list_all_calculated_channel_versions( order_by: str | None = None, limit: int | None = None, ) -> list[CalculatedChannel]: - """ - List all versions of a calculated channel. - """ + """List all versions of a calculated channel.""" return await self._handle_pagination( self.list_calculated_channel_versions, kwargs={ diff --git a/python/lib/sift_client/_internal/low_level_wrappers/channels.py b/python/lib/sift_client/_internal/low_level_wrappers/channels.py index f6d30de75..754a61cd3 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/channels.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/channels.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from sift.channels.v3.channels_pb2 import ( GetChannelRequest, @@ -12,9 +12,11 @@ from sift.channels.v3.channels_pb2_grpc import ChannelServiceStub from sift_client._internal.low_level_wrappers.base import LowLevelClientBase +from sift_client.sift_types.channel import Channel from sift_client.transport import WithGrpcClient -from sift_client.transport.grpc_transport import GrpcClient -from sift_client.types.channel import Channel + +if TYPE_CHECKING: + from sift_client.transport.grpc_transport import GrpcClient # Configure logging logger = logging.getLogger(__name__) @@ -23,15 +25,13 @@ class ChannelsLowLevelClient(LowLevelClientBase, WithGrpcClient): - """ - Low-level client for the ChannelsAPI. + """Low-level client for the ChannelsAPI. This class provides a thin wrapper around the autogenerated bindings for the ChannelsAPI. """ def __init__(self, grpc_client: GrpcClient): - """ - Initialize the ChannelsLowLevelClient. + """Initialize the ChannelsLowLevelClient. Args: grpc_client: The gRPC client to use for making API calls. @@ -39,8 +39,7 @@ def __init__(self, grpc_client: GrpcClient): super().__init__(grpc_client) async def get_channel(self, channel_id: str) -> Channel: - """ - Get a channel by channel_id. + """Get a channel by channel_id. Args: channel_id: The channel ID to get. @@ -51,10 +50,9 @@ async def get_channel(self, channel_id: str) -> Channel: Raises: ValueError: If channel_id is not provided. """ - request = GetChannelRequest(channel_id=channel_id) response = await self._grpc_client.get_stub(ChannelServiceStub).GetChannel(request) - grpc_channel = cast(GetChannelResponse, response).channel + grpc_channel = cast("GetChannelResponse", response).channel channel = Channel._from_proto(grpc_channel) return channel @@ -66,8 +64,7 @@ async def list_channels( query_filter: str | None = None, order_by: str | None = None, ) -> tuple[list[Channel], str]: - """ - List channels with optional filtering and pagination. + """List channels with optional filtering and pagination. Args: page_size: The maximum number of channels to return. @@ -78,7 +75,6 @@ async def list_channels( Returns: A tuple of (channels, next_page_token). """ - request_kwargs: dict[str, Any] = {} if query_filter: request_kwargs["filter"] = query_filter @@ -91,7 +87,7 @@ async def list_channels( request = ListChannelsRequest(**request_kwargs) response = await self._grpc_client.get_stub(ChannelServiceStub).ListChannels(request) - response = cast(ListChannelsResponse, response) + response = cast("ListChannelsResponse", response) channels = [Channel._from_proto(channel) for channel in response.channels] return channels, response.next_page_token @@ -103,8 +99,7 @@ async def list_all_channels( order_by: str | None = None, max_results: int | None = None, ) -> list[Channel]: - """ - List all channels with optional filtering. + """List all channels with optional filtering. Args: query_filter: A CEL filter string. diff --git a/python/lib/sift_client/_internal/low_level_wrappers/data.py b/python/lib/sift_client/_internal/low_level_wrappers/data.py index 0df283d73..e5370bbe7 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/data.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/data.py @@ -4,7 +4,7 @@ import logging from datetime import datetime, timezone from math import ceil -from typing import Any, List, Tuple, cast +from typing import TYPE_CHECKING, Any, cast import pandas as pd from pydantic import BaseModel, ConfigDict @@ -19,9 +19,11 @@ from sift_py._internal.time import to_timestamp_nanos from sift_client._internal.low_level_wrappers.base import LowLevelClientBase +from sift_client.sift_types.channel import Channel, ChannelDataType from sift_client.transport import WithGrpcClient -from sift_client.transport.grpc_transport import GrpcClient -from sift_client.types.channel import Channel, ChannelDataType + +if TYPE_CHECKING: + from sift_client.transport.grpc_transport import GrpcClient # Configure logging logger = logging.getLogger(__name__) @@ -47,8 +49,7 @@ class ChannelCache(BaseModel): class DataLowLevelClient(LowLevelClientBase, WithGrpcClient): - """ - Low-level client for fetching channel data. + """Low-level client for fetching channel data. This class provides a thin wrapper around the autogenerated bindings for the DataAPI. """ @@ -56,8 +57,7 @@ class DataLowLevelClient(LowLevelClientBase, WithGrpcClient): channel_cache: ChannelCache = ChannelCache(name_id_map={}, channels={}) def __init__(self, grpc_client: GrpcClient): - """ - Initialize the DataLowLevelClient. + """Initialize the DataLowLevelClient. Args: grpc_client: The gRPC client to use for making API calls. @@ -65,9 +65,7 @@ def __init__(self, grpc_client: GrpcClient): super().__init__(grpc_client) def _update_name_id_map(self, channels: list[Channel]): - """ - Update the name id map with the new channels. - """ + """Update the name id map with the new channels.""" for channel in channels: if channel.bit_field_elements: for bit_field_element in channel.bit_field_elements: @@ -88,10 +86,8 @@ async def _get_data_impl( page_size: int | None = None, page_token: str | None = None, order_by: str | None = None, - ) -> Tuple[List[Any], str | None]: - """ - Get the data for a channel during a run. - """ + ) -> tuple[list[Any], str | None]: + """Get the data for a channel during a run.""" queries = [ Query(channel=ChannelQuery(channel_id=channel_id, run_id=run_id)) for channel_id in channel_ids @@ -107,17 +103,17 @@ async def _get_data_impl( request = GetDataRequest(**request_kwargs) response = await self._grpc_client.get_stub(DataServiceStub).GetData(request) - response = cast(GetDataResponse, response) + response = cast("GetDataResponse", response) return response.data, response.next_page_token # type: ignore # mypy doesn't know RepeatedCompositeFieldContainer can be treated like a list - def _filter_cached_channels(self, channel_ids: List[str]) -> Tuple[List[str], List[str]]: + def _filter_cached_channels(self, channel_ids: list[str]) -> tuple[list[str], list[str]]: cached_channels = [] not_cached_channels = [] - for id in channel_ids: - if self.channel_cache.channels.get(id): - cached_channels.append(id) + for channel_id in channel_ids: + if self.channel_cache.channels.get(channel_id): + cached_channels.append(channel_id) else: - not_cached_channels.append(id) + not_cached_channels.append(channel_id) return cached_channels, not_cached_channels def _check_cache( @@ -127,9 +123,8 @@ def _check_cache( start_time: datetime, end_time: datetime, run_id: str | None = None, - ) -> Tuple[pd.DataFrame | None, datetime | None, datetime | None]: - """ - Check if the data for a channel during a run is cached and return how to query remaining data if so. + ) -> tuple[pd.DataFrame | None, datetime | None, datetime | None]: + """Check if the data for a channel during a run is cached and return how to query remaining data if so. There are a variety of requested start/end time vs cached start/end time cases to consider. Below diagram represents time aligned ranges for each case: @@ -188,9 +183,7 @@ def _update_cache( end_time: datetime, run_id: str | None = None, ): - """ - Update the cache with the new data and start/end times. - """ + """Update the cache with the new data and start/end times.""" assert start_time is not None assert end_time is not None name_id_map = self.channel_cache.name_id_map @@ -234,16 +227,14 @@ def _update_cache( async def get_channel_data( self, *, - channels: List[Channel], + channels: list[Channel], run_id: str | None = None, start_time: datetime | None = None, end_time: datetime | None = None, limit: int | None = None, ignore_cache: bool = False, ) -> dict[str, pd.DataFrame]: - """ - Get the data for a channel during a run. - """ + """Get the data for a channel during a run.""" ret_data = {} # No data will be returned if end_time is not provided. start_time = start_time or datetime.fromtimestamp(0, tz=timezone.utc) @@ -327,9 +318,7 @@ async def get_channel_data( @staticmethod def try_deserialize_channel_data(channel_data: Any) -> dict[str, pd.DataFrame]: - """ - Deserialize a channel data object into a numpy array. - """ + """Deserialize a channel data object into a numpy array.""" data_type = ChannelDataType.from_str(channel_data.type_url) if data_type is None: raise ValueError(f"Unknown data type: {channel_data.type_url}") @@ -340,13 +329,13 @@ def try_deserialize_channel_data(channel_data: Any) -> dict[str, pd.DataFrame]: ret_data = {} components = ( - proto_data_value.values if proto_data_class == BitFieldValues else [proto_data_value] + proto_data_value.values if proto_data_class is BitFieldValues else [proto_data_value] ) for component in components: name = metadata.channel.name time_column = [] value_column = [] - if proto_data_class == BitFieldValues: + if proto_data_class is BitFieldValues: name += "." + component.name for value_obj in component.values: time_column.append(to_timestamp_nanos(value_obj.timestamp)) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/ingestion.py b/python/lib/sift_client/_internal/low_level_wrappers/ingestion.py index 3eefe4ae2..da82e106a 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/ingestion.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/ingestion.py @@ -1,5 +1,4 @@ -""" -Low-level wrapper for the IngestionAPI. +"""Low-level wrapper for the IngestionAPI. This module provides thin wrappers around the autogenerated bindings for the IngestionAPI. It handles common concerns like error handling and retries. @@ -16,9 +15,8 @@ import threading import time from collections import namedtuple -from datetime import datetime from queue import Queue -from typing import Any, Dict, List, cast +from typing import TYPE_CHECKING, Any, cast import sift_stream_bindings from sift.ingestion_configs.v2.ingestion_configs_pb2 import ( @@ -36,18 +34,19 @@ from sift_client._internal.low_level_wrappers.base import ( LowLevelClientBase, ) +from sift_client.sift_types.ingestion import Flow, IngestionConfig, _to_rust_value from sift_client.transport import GrpcClient, WithGrpcClient -from sift_client.types.ingestion import Flow, IngestionConfig, _to_rust_value from sift_client.util import cel_utils as cel from sift_client.util.timestamp import to_rust_py_timestamp logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from datetime import datetime + class IngestionThread(threading.Thread): - """ - Manages ingestion for a single ingestion config. - """ + """Manages ingestion for a single ingestion config.""" IDLE_LOOP_PERIOD = 0.1 # Time of intervals loop will sleep while waiting for data. SIFT_STREAM_FINISH_TIMEOUT = 0.06 # Measured ~0.05s to finish stream. @@ -61,8 +60,7 @@ def __init__( no_data_timeout: int = 1, metric_interval: float = 0.5, ): - """ - Initialize the IngestionThread. + """Initialize the IngestionThread. Args: sift_stream_builder: The sift stream builder to build a new stream. @@ -148,8 +146,7 @@ def run(self): class IngestionLowLevelClient(LowLevelClientBase, WithGrpcClient): - """ - Low-level client for the IngestionAPI. + """Low-level client for the IngestionAPI. This class provides a thin wrapper around the autogenerated bindings for the IngestionAPI. It handles common concerns like error handling and retries. @@ -158,11 +155,10 @@ class IngestionLowLevelClient(LowLevelClientBase, WithGrpcClient): CacheEntry = namedtuple("CacheEntry", ["data_queue", "ingestion_config", "thread"]) sift_stream_builder: sift_stream_bindings.SiftStreamBuilderPy - stream_cache: Dict[str, "CacheEntry"] = {} + stream_cache: dict[str, CacheEntry] def __init__(self, grpc_client: GrpcClient): - """ - Initialize the IngestionLowLevelClient. + """Initialize the IngestionLowLevelClient. Args: grpc_client: The gRPC client to use for making API calls. @@ -183,17 +179,17 @@ def __init__(self, grpc_client: GrpcClient): sift_stream_bindings.RetryPolicyPy.default() ) ) + self.stream_cache = {} atexit.register(self.cleanup, timeout=0.1) def cleanup(self, timeout: float | None = None): - """ - Cleanup the ingestion threads. + """Cleanup the ingestion threads. Args: timeout: The timeout in seconds to wait for ingestion to complete. If None, will wait forever. """ - for _, cache_entry in self.stream_cache.items(): + for cache_entry in self.stream_cache.values(): data_queue, ingestion_config, thread = cache_entry # "None" value on the queue signals its loop to terminate. if thread: @@ -204,30 +200,24 @@ def cleanup(self, timeout: float | None = None): ) thread.stop() - async def get_ingestion_config_flows(self, ingestion_config_id: str) -> List[Flow]: - """ - Get the flows for an ingestion config. - """ + async def get_ingestion_config_flows(self, ingestion_config_id: str) -> list[Flow]: + """Get the flows for an ingestion config.""" res = await self._grpc_client.get_stub(IngestionConfigServiceStub).GetIngestionConfig( GetIngestionConfigRequest(ingestion_config_id=ingestion_config_id) ) - res = cast(ListIngestionConfigFlowsResponse, res) + res = cast("ListIngestionConfigFlowsResponse", res) return [Flow._from_proto(flow) for flow in res.flows] - async def list_ingestion_configs(self, filter_query: str) -> List[IngestionConfig]: - """ - List ingestion configs. - """ + async def list_ingestion_configs(self, filter_query: str) -> list[IngestionConfig]: + """List ingestion configs.""" res = await self._grpc_client.get_stub(IngestionConfigServiceStub).ListIngestionConfigs( ListIngestionConfigsRequest(filter=filter_query) ) - res = cast(ListIngestionConfigsResponse, res) + res = cast("ListIngestionConfigsResponse", res) return [IngestionConfig._from_proto(config) for config in res.ingestion_configs] async def get_ingestion_config_id_from_client_key(self, client_key: str) -> str | None: - """ - Get the ingestion config id. - """ + """Get the ingestion config id.""" filter_query = cel.equals("client_key", client_key) ingestion_configs = await self.list_ingestion_configs(filter_query) if not ingestion_configs: @@ -250,7 +240,7 @@ def _new_ingestion_thread( ingestion_config_id: The id of the ingestion config for the flows this stream will ingest. Used to cache the stream. ingestion_config: The ingestion config to use for ingestion. """ - data_queue: Queue[List[IngestWithConfigDataStreamRequestPy]] = Queue() + data_queue: Queue[list[IngestWithConfigDataStreamRequestPy]] = Queue() existing = self.stream_cache.get(ingestion_config_id) if existing: existing_data_queue, existing_ingestion_config, existing_thread = existing @@ -266,10 +256,8 @@ def _new_ingestion_thread( return self.CacheEntry(data_queue, ingestion_config, thread) - def _hash_flows(self, asset_name: str, flows: List[Flow]) -> str: - """ - Generate a client key that should be unique but deterministic for the given asset and flow configuration. - """ + def _hash_flows(self, asset_name: str, flows: list[Flow]) -> str: + """Generate a client key that should be unique but deterministic for the given asset and flow configuration.""" # TODO: Taken from sift_py/ingestion/config/telemetry.py. Confirm intent from Marc. m = hashlib.sha256() m.update(asset_name.encode()) @@ -300,12 +288,11 @@ async def create_ingestion_config( self, *, asset_name: str, - flows: List[Flow], + flows: list[Flow], client_key: str | None = None, organization_id: str | None = None, ) -> str: - """ - Create an ingestion config. + """Create an ingestion config. Args: asset_name: The name of the asset to ingest to. @@ -334,7 +321,7 @@ async def create_ingestion_config( logger.debug(f"Getting ingestion config id from generated client key {client_key}") ingestion_config_id = await self.get_ingestion_config_id_from_client_key(client_key) except ValueError: - logging.debug( + logger.debug( f"No ingestion config found for client key {client_key}. Creating new one." ) pass @@ -370,8 +357,7 @@ async def create_ingestion_config( return ingestion_config_id def wait_for_ingestion_to_complete(self, timeout: float | None = None): - """ - Blocks until all ingestion to complete. + """Blocks until all ingestion to complete. Args: timeout: The timeout in seconds to wait for ingestion to complete. If None, will wait forever. @@ -387,8 +373,7 @@ def ingest_flow( channel_values: dict[str, Any], organization_id: str | None = None, ): - """ - Ingest a flow. This is a synchronous call that queues an ingestion request that will be processed asynchronously on a background thread. + """Ingest a flow. This is a synchronous call that queues an ingestion request that will be processed asynchronously on a background thread. Args: flow: The flow to ingest. @@ -396,7 +381,6 @@ def ingest_flow( channel_values: The channel values to ingest. organization_id: The organization id to use for ingestion. Only relevant if the user is part of several organizations. """ - if not flow.ingestion_config_id: raise ValueError( "Flow has no ingestion config id -- have you created an ingestion config for this flow?" diff --git a/python/lib/sift_client/_internal/low_level_wrappers/ping.py b/python/lib/sift_client/_internal/low_level_wrappers/ping.py index 490a2b422..650f2d44a 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/ping.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/ping.py @@ -1,5 +1,4 @@ -""" -Low-level wrapper for the PingAPI. +"""Low-level wrapper for the PingAPI. This module provides thin wrappers around the autogenerated bindings for the PingAPI. It handles common concerns like error handling and retries. @@ -28,16 +27,14 @@ class PingLowLevelClient(LowLevelClientBase, WithGrpcClient): - """ - Low-level client for the PingAPI. + """Low-level client for the PingAPI. This class provides a thin wrapper around the autogenerated bindings for the PingAPI. It handles common concerns like error handling and retries. """ def __init__(self, grpc_client: GrpcClient): - """ - Initialize the PingLowLevelClient. + """Initialize the PingLowLevelClient. Args: grpc_client: The gRPC client to use for making API calls. @@ -45,11 +42,9 @@ def __init__(self, grpc_client: GrpcClient): super().__init__(grpc_client=grpc_client) async def ping(self) -> str: - """ - Send a ping request to the server in the current event loop. - """ + """Send a ping request to the server in the current event loop.""" # get stub bound to this loop stub = self._grpc_client.get_stub(PingServiceStub) request = PingRequest() response = await stub.Ping(request) - return cast(PingResponse, response).response + return cast("PingResponse", response).response diff --git a/python/lib/sift_client/_internal/low_level_wrappers/rules.py b/python/lib/sift_client/_internal/low_level_wrappers/rules.py index 2600bc8e0..676b5fa71 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/rules.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/rules.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import Any, List, cast +from typing import TYPE_CHECKING, Any, cast from sift.rules.v1.rules_pb2 import ( BatchDeleteRulesRequest, @@ -31,28 +31,28 @@ from sift.rules.v1.rules_pb2_grpc import RuleServiceStub from sift_client._internal.low_level_wrappers.base import LowLevelClientBase -from sift_client.transport import GrpcClient, WithGrpcClient -from sift_client.types.channel import ChannelReference -from sift_client.types.rule import ( +from sift_client.sift_types.rule import ( Rule, RuleAction, RuleUpdate, ) +from sift_client.transport import GrpcClient, WithGrpcClient + +if TYPE_CHECKING: + from sift_client.sift_types.channel import ChannelReference # Configure logging logger = logging.getLogger(__name__) class RulesLowLevelClient(LowLevelClientBase, WithGrpcClient): - """ - Low-level client for the RulesAPI. + """Low-level client for the RulesAPI. This class provides a thin wrapper around the autogenerated bindings for the RulesAPI. """ def __init__(self, grpc_client: GrpcClient): - """ - Initialize the RulesLowLevelClient. + """Initialize the RulesLowLevelClient. Args: grpc_client: The gRPC client to use for making API calls. @@ -60,8 +60,7 @@ def __init__(self, grpc_client: GrpcClient): super().__init__(grpc_client) async def get_rule(self, rule_id: str | None = None, client_key: str | None = None) -> Rule: - """ - Get a rule by rule_id or client_key. + """Get a rule by rule_id or client_key. Args: rule_id: The rule ID to get. @@ -81,14 +80,13 @@ async def get_rule(self, rule_id: str | None = None, client_key: str | None = No request = GetRuleRequest(**request_kwargs) response = await self._grpc_client.get_stub(RuleServiceStub).GetRule(request) - grpc_rule = cast(GetRuleResponse, response).rule + grpc_rule = cast("GetRuleResponse", response).rule return Rule._from_proto(grpc_rule) async def batch_get_rules( self, rule_ids: list[str] | None = None, client_keys: list[str] | None = None ) -> list[Rule]: - """ - Get multiple rules by rule_ids or client_keys. + """Get multiple rules by rule_ids or client_keys. Args: rule_ids: List of rule IDs to get. @@ -111,7 +109,7 @@ async def batch_get_rules( request = BatchGetRulesRequest(**request_kwargs) response = await self._grpc_client.get_stub(RuleServiceStub).BatchGetRules(request) - response = cast(BatchGetRulesResponse, response) + response = cast("BatchGetRulesResponse", response) return [Rule._from_proto(rule) for rule in response.rules] async def create_rule( @@ -126,11 +124,10 @@ async def create_rule( contextual_channels: list[str] | None = None, is_external: bool, expression: str, - channel_references: List[ChannelReference], + channel_references: list[ChannelReference], action: RuleAction, ) -> Rule: - """ - Create a new rule. + """Create a new rule. Args: name: The name of the rule. @@ -177,7 +174,7 @@ async def create_rule( request = CreateRuleRequest(update=update_request) created_rule = cast( - CreateRuleResponse, + "CreateRuleResponse", await self._grpc_client.get_stub(RuleServiceStub).CreateRule(request), ) return await self.get_rule(rule_id=created_rule.rule_id, client_key=client_key) @@ -185,8 +182,7 @@ async def create_rule( def _update_rule_request_from_update( self, rule: Rule, update: RuleUpdate, version_notes: str | None = None ) -> UpdateRuleRequest: - """ - Create an update request from a rule and update. + """Create an update request from a rule and update. This helper exists because the Rule update protos need a pattern that is less generic than the normal update + mask pattern of other types. """ @@ -209,9 +205,13 @@ def _update_rule_request_from_update( ] # Populate the trivial fields first. - for updated_field, value in model_dump.items(): - if updated_field not in nontrivial_updates: - update_dict[updated_field] = value + update_dict.update( + { + updated_field: value + for updated_field, value in model_dump.items() + if updated_field not in nontrivial_updates + } + ) # Populate the fields that weren't updated but will be reset if not provided in request. for field in copy_unset_fields: if field not in model_dump: @@ -220,7 +220,7 @@ def _update_rule_request_from_update( # Special handling for the more complex fields. # Also, these must always be set. expression = model_dump.get("expression", rule.expression) - channel_references: List[ChannelReference] = ( + channel_references: list[ChannelReference] = ( update.channel_references if "channel_references" in model_dump else rule.channel_references @@ -271,13 +271,13 @@ def _update_rule_request_from_update( async def update_rule( self, rule: Rule, update: RuleUpdate, version_notes: str | None = None ) -> Rule: - """ - Update a rule. + """Update a rule. Args: rule: The rule to update. update: The update to apply. version_notes: Notes to include in the rule version. + Returns: The updated Rule. """ @@ -286,13 +286,12 @@ async def update_rule( update_request = self._update_rule_request_from_update(rule, update, version_notes) response = await self._grpc_client.get_stub(RuleServiceStub).UpdateRule(update_request) - updated_grpc_rule = cast(UpdateRuleResponse, response) + updated_grpc_rule = cast("UpdateRuleResponse", response) # Get the updated rule return await self.get_rule(rule_id=updated_grpc_rule.rule_id) async def batch_update_rules(self, rules: list[RuleUpdate]) -> BatchUpdateRulesResponse: - """ - Batch update rules. + """Batch update rules. Args: rules: List of rule updates to apply. @@ -308,11 +307,10 @@ async def batch_update_rules(self, rules: list[RuleUpdate]) -> BatchUpdateRulesR request = BatchUpdateRulesRequest(rules=update_requests) # type: ignore response = await self._grpc_client.get_stub(RuleServiceStub).BatchUpdateRules(request) - return cast(BatchUpdateRulesResponse, response) + return cast("BatchUpdateRulesResponse", response) async def archive_rule(self, rule_id: str | None = None, client_key: str | None = None) -> None: - """ - Archive a rule. + """Archive a rule. Args: rule_id: The rule ID to archive. @@ -334,10 +332,9 @@ async def archive_rule(self, rule_id: str | None = None, client_key: str | None await self._grpc_client.get_stub(RuleServiceStub).ArchiveRule(request) async def batch_archive_rules( - self, rule_ids: List[str] | None = None, client_keys: List[str] | None = None + self, rule_ids: list[str] | None = None, client_keys: list[str] | None = None ) -> None: - """ - Batch archive rules. + """Batch archive rules. Args: rule_ids: List of rule IDs to archive. @@ -359,8 +356,7 @@ async def batch_archive_rules( await self._grpc_client.get_stub(RuleServiceStub).BatchDeleteRules(request) async def restore_rule(self, rule_id: str | None = None, client_key: str | None = None) -> Rule: - """ - Restore a rule. + """Restore a rule. Args: rule_id: The rule ID to restore. @@ -387,10 +383,9 @@ async def restore_rule(self, rule_id: str | None = None, client_key: str | None return await self.get_rule(rule_id=rule_id, client_key=client_key) async def batch_restore_rules( - self, rule_ids: List[str] | None = None, client_keys: List[str] | None = None + self, rule_ids: list[str] | None = None, client_keys: list[str] | None = None ) -> None: - """ - Batch restore rules. + """Batch restore rules. Args: rule_ids: List of rule IDs to restore. @@ -418,10 +413,8 @@ async def list_rules( order_by: str | None = None, page_size: int | None = None, page_token: str | None = None, - ) -> tuple[List[Rule], str | None]: - """ - List rules. - """ + ) -> tuple[list[Rule], str | None]: + """List rules.""" request_kwargs: dict[str, Any] = {} if filter_query is not None: request_kwargs["filter"] = filter_query @@ -443,10 +436,8 @@ async def list_all_rules( order_by: str | None = None, max_results: int | None = None, page_size: int | None = None, - ) -> List[Rule]: - """ - List all rules. - """ + ) -> list[Rule]: + """List all rules.""" return await self._handle_pagination( self.list_rules, kwargs={"filter_query": filter_query}, diff --git a/python/lib/sift_client/_internal/low_level_wrappers/runs.py b/python/lib/sift_client/_internal/low_level_wrappers/runs.py index 66631c7ee..067d99c7f 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/runs.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/runs.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from sift.runs.v2.runs_pb2 import ( CreateAutomaticRunAssociationForAssetsRequest, @@ -19,25 +19,25 @@ from sift.runs.v2.runs_pb2_grpc import RunServiceStub from sift_client._internal.low_level_wrappers.base import LowLevelClientBase +from sift_client.sift_types.run import Run, RunUpdate from sift_client.transport import WithGrpcClient -from sift_client.transport.grpc_transport import GrpcClient -from sift_client.types.run import Run, RunUpdate from sift_client.util.metadata import metadata_dict_to_proto +if TYPE_CHECKING: + from sift_client.transport.grpc_transport import GrpcClient + # Configure logging logger = logging.getLogger(__name__) class RunsLowLevelClient(LowLevelClientBase, WithGrpcClient): - """ - Low-level client for the RunsAPI. + """Low-level client for the RunsAPI. This class provides a thin wrapper around the autogenerated bindings for the RunsAPI. """ def __init__(self, grpc_client: GrpcClient): - """ - Initialize the RunsLowLevelClient. + """Initialize the RunsLowLevelClient. Args: grpc_client: The gRPC client to use for making API calls. @@ -45,8 +45,7 @@ def __init__(self, grpc_client: GrpcClient): super().__init__(grpc_client) async def get_run(self, run_id: str) -> Run: - """ - Get a run by run_id. + """Get a run by run_id. Args: run_id: The run ID to get. @@ -62,7 +61,7 @@ async def get_run(self, run_id: str) -> Run: request = GetRunRequest(run_id=run_id) response = await self._grpc_client.get_stub(RunServiceStub).GetRun(request) - grpc_run = cast(GetRunResponse, response).run + grpc_run = cast("GetRunResponse", response).run return Run._from_proto(grpc_run) async def list_runs( @@ -73,8 +72,7 @@ async def list_runs( query_filter: str | None = None, order_by: str | None = None, ) -> tuple[list[Run], str]: - """ - List runs with optional filtering and pagination. + """List runs with optional filtering and pagination. Args: page_size: The maximum number of runs to return. @@ -97,7 +95,7 @@ async def list_runs( request = ListRunsRequest(**request_kwargs) response = await self._grpc_client.get_stub(RunServiceStub).ListRuns(request) - response = cast(ListRunsResponse, response) + response = cast("ListRunsResponse", response) runs = [Run._from_proto(run) for run in response.runs] return runs, response.next_page_token @@ -109,8 +107,7 @@ async def list_all_runs( order_by: str | None = None, max_results: int | None = None, ) -> list[Run]: - """ - List all runs with optional filtering. + """List all runs with optional filtering. Args: query_filter: A CEL filter string. @@ -139,8 +136,7 @@ async def create_run( client_key: str | None = None, metadata: dict[str, str | float | bool] | None = None, ) -> Run: - """ - Create a new run. + """Create a new run. Args: name: The name of the run. @@ -176,12 +172,11 @@ async def create_run( request = CreateRunRequest(**request_kwargs) response = await self._grpc_client.get_stub(RunServiceStub).CreateRun(request) - grpc_run = cast(CreateRunResponse, response).run + grpc_run = cast("CreateRunResponse", response).run return Run._from_proto(grpc_run) async def update_run(self, run: Run, update: RunUpdate) -> Run: - """ - Update an existing run. + """Update an existing run. Args: run: The run to update. @@ -194,12 +189,11 @@ async def update_run(self, run: Run, update: RunUpdate) -> Run: request = UpdateRunRequest(run=run_proto, update_mask=field_mask) response = await self._grpc_client.get_stub(RunServiceStub).UpdateRun(request) - grpc_run = cast(UpdateRunResponse, response).run + grpc_run = cast("UpdateRunResponse", response).run return Run._from_proto(grpc_run) async def archive_run(self, run_id: str) -> None: - """ - Archive a run. + """Archive a run. Args: run_id: The ID of the run to archive. @@ -214,8 +208,7 @@ async def archive_run(self, run_id: str) -> None: await self._grpc_client.get_stub(RunServiceStub).DeleteRun(request) async def stop_run(self, run_id: str) -> None: - """ - Stop a run by setting its stop time to the current time. + """Stop a run by setting its stop time to the current time. Args: run_id: The ID of the run to stop. @@ -232,8 +225,7 @@ async def stop_run(self, run_id: str) -> None: async def create_automatic_run_association_for_assets( self, run_id: str, asset_names: list[str] ) -> None: - """ - Associate assets with a run for automatic data ingestion. + """Associate assets with a run for automatic data ingestion. Args: run_id: The ID of the run. diff --git a/python/lib/sift_client/_internal/sync_wrapper.py b/python/lib/sift_client/_internal/sync_wrapper.py index 323f60c3e..b5ce3f43d 100644 --- a/python/lib/sift_client/_internal/sync_wrapper.py +++ b/python/lib/sift_client/_internal/sync_wrapper.py @@ -1,6 +1,4 @@ -""" -Utility for generating synchronous API wrappers from asynchronous API classes. -""" +"""Utility for generating synchronous API wrappers from asynchronous API classes.""" from __future__ import annotations @@ -8,17 +6,18 @@ import inspect import sys from functools import wraps -from typing import Any, Type, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from typing_extensions import TypedDict -from sift_client.resources._base import ResourceBase +if TYPE_CHECKING: + from sift_client.resources._base import ResourceBase # registry of all classes decorated with @generate_sync_api class SyncAPIRegistration(TypedDict): - async_cls: Type[Any] - sync_cls: Type[Any] + async_cls: type[Any] + sync_cls: type[Any] _registered: list[SyncAPIRegistration] = [] @@ -26,9 +25,8 @@ class SyncAPIRegistration(TypedDict): S = TypeVar("S") -def generate_sync_api(cls: Type[ResourceBase], sync_name: str) -> type: - """ - Generate a synchronous wrapper class for the given async API class. +def generate_sync_api(cls: type[ResourceBase], sync_name: str) -> type: + """Generate a synchronous wrapper class for the given async API class. It creates a new class whose name is derived from the async class by stripping a trailing 'Async' (e.g. PingAPIAsync -> PingAPI). For each @@ -137,7 +135,7 @@ def sync_prop(self, _prop_name=prop_name): namespace[name] = _wrap_sync(name) # Create the sync class - sync_class = type(sync_name, (object,), namespace) # noqa + sync_class = type(sync_name, (object,), namespace) # Register the class in the module's globals # This helps static analysis tools recognize it as a proper class diff --git a/python/lib/sift_client/_tests/_internal/test_gen_pyi.py b/python/lib/sift_client/_tests/_internal/test_gen_pyi.py index 5db29d1a3..1c39d7fbc 100644 --- a/python/lib/sift_client/_tests/_internal/test_gen_pyi.py +++ b/python/lib/sift_client/_tests/_internal/test_gen_pyi.py @@ -18,7 +18,7 @@ def generated(): generated = generate_stubs_for_module(pathlib.Path(__file__).parent / "test_stub_module") assert len(generated) == 1, "test_ file should be excluded" - return list(generated.values())[0] + return next(iter(generated.values())) def test_extract_imports(generated): @@ -29,7 +29,7 @@ def test_extract_imports(generated): assert "Auto-generated" in import_section assert "from __future__ import annotations" in import_section - assert "from sift_client.types.asset import Asset" in import_section + assert "from sift_client.sift_types.asset import Asset" in import_section assert "from sift_client.resources._base import ResourceBase" in import_section diff --git a/python/lib/sift_client/_tests/_internal/test_stub_module/test_py.py b/python/lib/sift_client/_tests/_internal/test_stub_module/test_py.py index 379a81b3f..e5f984484 100644 --- a/python/lib/sift_client/_tests/_internal/test_stub_module/test_py.py +++ b/python/lib/sift_client/_tests/_internal/test_stub_module/test_py.py @@ -1,7 +1,11 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from sift_client.resources._base import ResourceBase -from sift_client.types.asset import Asset + +if TYPE_CHECKING: + from sift_client.sift_types.asset import Asset class MockClassAsync(ResourceBase): @@ -38,8 +42,8 @@ def sync_prop(self) -> int: class SecondMockClass: - """Class doesn't have a sync version generated so shouldn't be present""" + """Class doesn't have a sync version generated so shouldn't be present.""" def shouldnt_be_in_gen_stubs(self): - """Shouldn't be in gen stubs since it isn't called by generator""" + """Shouldn't be in gen stubs since it isn't called by generator.""" return diff --git a/python/lib/sift_client/_tests/_internal/test_sync_wrapper.py b/python/lib/sift_client/_tests/_internal/test_sync_wrapper.py index 62623b989..86841f2b6 100644 --- a/python/lib/sift_client/_tests/_internal/test_sync_wrapper.py +++ b/python/lib/sift_client/_tests/_internal/test_sync_wrapper.py @@ -4,7 +4,7 @@ import atexit import inspect import threading -from typing import Any, Dict, Optional +from typing import Any import pytest @@ -48,7 +48,7 @@ class MockResourceAsync(ResourceBase): def __init__(self, client=None, value: str = "default"): super().__init__(client) self._value = value - self._calls: Dict[str, int] = {} + self._calls: dict[str, int] = {} @property def value(self) -> str: @@ -83,8 +83,8 @@ async def async_method_with_exception(self) -> None: raise ValueError("Test exception") async def async_method_with_complex_args( - self, arg1: str, arg2: Optional[Dict[str, Any]] = None, *args, **kwargs - ) -> Dict[str, Any]: + self, arg1: str, arg2: dict[str, Any] | None = None, *args, **kwargs + ) -> dict[str, Any]: """Test asynchronous method with complex arguments.""" self._record_call("async_method_with_complex_args") await asyncio.sleep(0.01) diff --git a/python/lib/sift_client/_tests/integrated/__init__.py b/python/lib/sift_client/_tests/integrated/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/lib/sift_client/_tests/integrated/calculated_channels.py b/python/lib/sift_client/_tests/integrated/calculated_channels.py index ce887fb47..41a01318a 100644 --- a/python/lib/sift_client/_tests/integrated/calculated_channels.py +++ b/python/lib/sift_client/_tests/integrated/calculated_channels.py @@ -1,11 +1,11 @@ import asyncio import os -from datetime import datetime +from datetime import datetime, timezone from sift_client.client import SiftClient # Import sift_client types for calculated channels and rules -from sift_client.types import ( +from sift_client.sift_types import ( CalculatedChannelUpdate, ChannelReference, ) @@ -48,7 +48,7 @@ async def main(): # Create example calculated channels that will be unique to this test run in case things don't cleanup. num_channels = 7 - unique_name_suffix = datetime.now().strftime("%Y%m%d%H%M%S") + unique_name_suffix = datetime.now(tz=timezone.utc).strftime("%Y%m%d%H%M%S") print( f"\n=== Creating {num_channels} calculated channels with unique suffix: {unique_name_suffix} ===" ) diff --git a/python/lib/sift_client/_tests/integrated/channels.py b/python/lib/sift_client/_tests/integrated/channels.py index 4b4247e5c..2a721a4bf 100644 --- a/python/lib/sift_client/_tests/integrated/channels.py +++ b/python/lib/sift_client/_tests/integrated/channels.py @@ -6,6 +6,7 @@ import numpy as np import pandas as pd import pyarrow as pa + from sift_client.client import SiftClient diff --git a/python/lib/sift_client/_tests/integrated/ingestion.py b/python/lib/sift_client/_tests/integrated/ingestion.py index 4045eae98..87e9e54e2 100644 --- a/python/lib/sift_client/_tests/integrated/ingestion.py +++ b/python/lib/sift_client/_tests/integrated/ingestion.py @@ -3,17 +3,17 @@ import os import random import time -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from sift_client._tests import setup_logger from sift_client.client import SiftClient -from sift_client.transport import SiftConnectionConfig -from sift_client.types.channel import ( +from sift_client.sift_types.channel import ( Channel, ChannelBitFieldElement, ChannelDataType, ) -from sift_client.types.ingestion import Flow +from sift_client.sift_types.ingestion import Flow +from sift_client.transport import SiftConnectionConfig setup_logger() @@ -45,7 +45,7 @@ async def main(): client.runs.archive(run=run) run = client.runs.create( - name=f"test-run-{datetime.now().timestamp()}", + name=f"test-run-{datetime.now(tz=timezone.utc).timestamp()}", description="A test run created via the API", tags=["api-created", "test"], ) @@ -110,7 +110,7 @@ async def main(): simulated_duration = 50 fake_hs_rate = 50 # Hz fake_hs_period = 1 / fake_hs_rate - start = datetime.now() + start = datetime.now(tz=timezone.utc) for i in range(simulated_duration): now = start + timedelta(seconds=i) regular_flow.ingest( @@ -186,7 +186,7 @@ async def main(): ) client.async_.ingestion.wait_for_ingestion_to_complete(timeout=2) - end = datetime.now() + end = datetime.now(tz=timezone.utc) # Test ingesting more data after letting a thread finish. Also exercise ingesting bitfield values as bytes. time.sleep(1) print("Restarting ingestion") diff --git a/python/lib/sift_client/_tests/integrated/rules.py b/python/lib/sift_client/_tests/integrated/rules.py index b41eb77c4..85c1e1ddc 100644 --- a/python/lib/sift_client/_tests/integrated/rules.py +++ b/python/lib/sift_client/_tests/integrated/rules.py @@ -1,10 +1,10 @@ import os -from datetime import datetime +from datetime import datetime, timezone from sift_client.client import SiftClient # Import sift_client types for calculated channels and rules -from sift_client.types import ( +from sift_client.sift_types import ( ChannelReference, RuleAction, RuleAnnotationType, @@ -45,7 +45,7 @@ def main(): asset_id = asset.id_ print(f"Using asset: {asset.name} (ID: {asset_id})") - unique_name_suffix = datetime.now().strftime("%Y%m%d%H%M%S") + unique_name_suffix = datetime.now(tz=timezone.utc).strftime("%Y%m%d%H%M%S") num_rules = 8 print(f"\n=== Creating {num_rules} rules with unique suffix: {unique_name_suffix} ===") created_rules = [] diff --git a/python/lib/sift_client/_tests/integrated/runs.py b/python/lib/sift_client/_tests/integrated/runs.py index a620f713c..7a8cdf19b 100644 --- a/python/lib/sift_client/_tests/integrated/runs.py +++ b/python/lib/sift_client/_tests/integrated/runs.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -This test demonstrates the usage of the Runs API. +"""This test demonstrates the usage of the Runs API. It creates a new run, updates it, and associates assets with it. It also lists runs, filters them, and deletes the run. @@ -10,15 +9,13 @@ import asyncio import os -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from sift_client import SiftClient async def main(): - """ - Main function demonstrating the Runs API usage. - """ + """Main function demonstrating the Runs API usage.""" # Initialize the client # You can set these environment variables or pass them directly grpc_url = os.getenv("SIFT_GRPC_URI", "localhost:50051") @@ -206,7 +203,7 @@ async def main(): } # Create a run with start and stop times - start_time = datetime.now() + start_time = datetime.now(timezone.utc) stop_time = start_time + timedelta(minutes=2) previously_created_runs = client.runs.list(name_regex="Example Test Run.*") @@ -217,13 +214,13 @@ async def main(): client.runs.archive(run=run) new_run = client.runs.create( - name=f"Example Test Run {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + name=f"Example Test Run {datetime.now(tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}", description="A test run created via the API", tags=["api-created", "test"], start_time=start_time, stop_time=stop_time, # Use a unique client key for each run - client_key=f"example-run-key-{datetime.now().timestamp()}", + client_key=f"example-run-key-{datetime.now(tz=timezone.utc).timestamp()}", metadata=metadata, ) print(f" Created run: {new_run.name} (ID: {new_run.id_})") diff --git a/python/lib/sift_client/_tests/util/test_cel_utils.py b/python/lib/sift_client/_tests/util/test_cel_utils.py index 665f7a058..f2f0cac9d 100644 --- a/python/lib/sift_client/_tests/util/test_cel_utils.py +++ b/python/lib/sift_client/_tests/util/test_cel_utils.py @@ -1,5 +1,5 @@ import re -from datetime import datetime +from datetime import datetime, timezone from sift_client.util.cel_utils import ( and_, @@ -153,7 +153,7 @@ def test_greater_than_number(self): def test_greater_than_datetime(self): """Test greater_than function with datetime value.""" - dt = datetime(2023, 1, 1, 12, 0, 0) + dt = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc) assert greater_than("field", dt) == f"field > {dt.isoformat()}" def test_less_than_number(self): @@ -163,5 +163,5 @@ def test_less_than_number(self): def test_less_than_datetime(self): """Test less_than function with datetime value.""" - dt = datetime(2023, 1, 1, 12, 0, 0) + dt = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc) assert less_than("field", dt) == f"field < {dt.isoformat()}" diff --git a/python/lib/sift_client/client.py b/python/lib/sift_client/client.py index 141d20b91..427e4def5 100644 --- a/python/lib/sift_client/client.py +++ b/python/lib/sift_client/client.py @@ -34,8 +34,7 @@ class SiftClient( WithGrpcClient, WithRestClient, ): - """ - SiftClient is a high-level client for interacting with Sift's APIs. + """SiftClient is a high-level client for interacting with Sift's APIs. It provides both synchronous and asynchronous interfaces, strong type checking, and a Pythonic API design. @@ -97,8 +96,7 @@ def __init__( rest_url: str | None = None, connection_config: SiftConnectionConfig | None = None, ): - """ - Initialize the SiftClient with specific connection parameters or a connection_config. + """Initialize the SiftClient with specific connection parameters or a connection_config. Args: api_key: The Sift API key for authentication. @@ -106,7 +104,6 @@ def __init__( rest_url: The Sift REST API URL. connection_config: A SiftConnectionConfig object to configure the connection behavior of the SiftClient. """ - if not (api_key and grpc_url and rest_url) and not connection_config: raise ValueError( "Either api_key, grpc_url and rest_url or connection_config must be provided to establish a connection." diff --git a/python/lib/sift_client/errors.py b/python/lib/sift_client/errors.py index 9ebb1babf..471d3898f 100644 --- a/python/lib/sift_client/errors.py +++ b/python/lib/sift_client/errors.py @@ -4,19 +4,23 @@ class SiftWarning(UserWarning): - """ - Base warning for Sift generated warnings. - """ + """Base warning for Sift generated warnings.""" class SiftExperimentalWarning(SiftWarning): - """ - Warning for experimental features. - """ + """Warning for experimental features.""" + + +_sift_client_experimental_warned = False def _sift_client_experimental_warning(): - warnings.warn( - "`sift_client` is experimental and is subject to change. Use with caution.", - SiftExperimentalWarning, - ) + # Ensure this warning has only been emitted once, even if used in different places. + global _sift_client_experimental_warned + if not _sift_client_experimental_warned: + warnings.warn( + "`sift_client` is experimental and is subject to change. Use with caution.", + SiftExperimentalWarning, + stacklevel=2, + ) + _sift_client_experimental_warned = True diff --git a/python/lib/sift_client/examples/__init__.py b/python/lib/sift_client/examples/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/lib/sift_client/examples/generic_workflow_example.py b/python/lib/sift_client/examples/generic_workflow_example.py index c060b2a68..307c00547 100644 --- a/python/lib/sift_client/examples/generic_workflow_example.py +++ b/python/lib/sift_client/examples/generic_workflow_example.py @@ -1,11 +1,11 @@ import asyncio import os -from datetime import datetime +from datetime import datetime, timezone from sift_client.client import SiftClient # Import sift_client types for calculated channels and rules -from sift_client.types import ( +from sift_client.sift_types import ( CalculatedChannelUpdate, ChannelReference, RuleAction, @@ -80,7 +80,7 @@ async def main(): print(f"Updating rule: {rule.name}") rule = rule.update( RuleUpdate( - description=f"Alert when velocity-to-voltage ratio exceeds 0.1 (Updated at {datetime.now().isoformat()})", + description=f"Alert when velocity-to-voltage ratio exceeds 0.1 (Updated at {datetime.now(tz=timezone.utc).isoformat()})", asset_ids=[asset_id], ) ) diff --git a/python/lib/sift_client/resources/__init__.py b/python/lib/sift_client/resources/__init__.py index 7808366a0..5997acb02 100644 --- a/python/lib/sift_client/resources/__init__.py +++ b/python/lib/sift_client/resources/__init__.py @@ -15,17 +15,17 @@ ) __all__ = [ + "AssetsAPI", "AssetsAPIAsync", + "CalculatedChannelsAPI", "CalculatedChannelsAPIAsync", + "ChannelsAPI", "ChannelsAPIAsync", "IngestionAPIAsync", - "PingAPIAsync", - "RulesAPIAsync", - "RunsAPIAsync", - "AssetsAPI", - "CalculatedChannelsAPI", - "ChannelsAPI", "PingAPI", + "PingAPIAsync", "RulesAPI", + "RulesAPIAsync", "RunsAPI", + "RunsAPIAsync", ] diff --git a/python/lib/sift_client/resources/_base.py b/python/lib/sift_client/resources/_base.py index 8ef62bc37..2170aad64 100644 --- a/python/lib/sift_client/resources/_base.py +++ b/python/lib/sift_client/resources/_base.py @@ -4,25 +4,25 @@ from typing import TYPE_CHECKING, TypeVar from sift_client.errors import _sift_client_experimental_warning -from sift_client.transport.base_connection import GrpcClient, RestClient _sift_client_experimental_warning() if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.types._base import BaseType + from sift_client.sift_types._base import BaseType + from sift_client.transport.base_connection import GrpcClient, RestClient T = TypeVar("T", bound="BaseType") class ResourceBase(ABC): - _sift_client: "SiftClient" + _sift_client: SiftClient - def __init__(self, sift_client: "SiftClient"): + def __init__(self, sift_client: SiftClient): self._sift_client = sift_client @property - def client(self) -> "SiftClient": + def client(self) -> SiftClient: return self._sift_client @property diff --git a/python/lib/sift_client/resources/assets.py b/python/lib/sift_client/resources/assets.py index dc7a35235..dec34bfcb 100644 --- a/python/lib/sift_client/resources/assets.py +++ b/python/lib/sift_client/resources/assets.py @@ -1,21 +1,21 @@ from __future__ import annotations -import re -from datetime import datetime from typing import TYPE_CHECKING, Any from sift_client._internal.low_level_wrappers.assets import AssetsLowLevelClient from sift_client.resources._base import ResourceBase -from sift_client.types.asset import Asset, AssetUpdate +from sift_client.sift_types.asset import Asset, AssetUpdate from sift_client.util import cel_utils if TYPE_CHECKING: + import re + from datetime import datetime + from sift_client.client import SiftClient class AssetsAPIAsync(ResourceBase): - """ - High-level API for interacting with assets. + """High-level API for interacting with assets. This class provides a Pythonic, notebook-friendly interface for interacting with the AssetsAPI. It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. @@ -24,9 +24,8 @@ class AssetsAPIAsync(ResourceBase): representation of an asset using standard Python data structures and types. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the AssetsAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the AssetsAPI. Args: sift_client: The Sift client to use. @@ -40,8 +39,7 @@ async def get( asset_id: str | None = None, name: str | None = None, ) -> Asset: - """ - Get an Asset. + """Get an Asset. Args: asset_id: The ID of the asset. @@ -91,8 +89,7 @@ async def list_( order_by: str | None = None, limit: int | None = None, ) -> list[Asset]: - """ - List assets with optional filtering. + """List assets with optional filtering. Args: asset_ids: List of asset IDs to filter by. @@ -108,6 +105,7 @@ async def list_( modified_by: Assets last modified by this user. tags: Assets with these tags. tag_ids: List of asset tag IDs to filter by. + metadata: metadata filter include_archived: Include archived assets. filter_query: Explicit CEL query to filter assets. order_by: How to order the retrieved assets. # TODO: tooling for this? @@ -157,8 +155,7 @@ async def list_( return self._apply_client_to_instances(assets) async def find(self, **kwargs) -> Asset | None: - """ - Find a single asset matching the given query. Takes the same arguments as `list_`. If more than one asset is found, + """Find a single asset matching the given query. Takes the same arguments as `list_`. If more than one asset is found, raises an error. Args: @@ -175,14 +172,13 @@ async def find(self, **kwargs) -> Asset | None: return None async def archive(self, asset: str | Asset, *, archive_runs: bool = False) -> Asset: - """ - Archive an asset. + """Archive an asset. - Args: + Args: asset: The Asset or asset ID to archive. archive_runs: If True, archive all Runs associated with the Asset. - Returns: + Returns: The archived Asset. """ asset_id = asset.id_ or "" if isinstance(asset, Asset) else asset @@ -192,8 +188,7 @@ async def archive(self, asset: str | Asset, *, archive_runs: bool = False) -> As return await self.get(asset_id=asset_id) async def update(self, asset: str | Asset, update: AssetUpdate | dict) -> Asset: - """ - Update an Asset. + """Update an Asset. Args: asset: The Asset or asset ID to update. diff --git a/python/lib/sift_client/resources/calculated_channels.py b/python/lib/sift_client/resources/calculated_channels.py index b248936d3..7c786ebf9 100644 --- a/python/lib/sift_client/resources/calculated_channels.py +++ b/python/lib/sift_client/resources/calculated_channels.py @@ -1,27 +1,27 @@ from __future__ import annotations -import re -from datetime import datetime -from typing import TYPE_CHECKING, Any, List +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any from sift_client._internal.low_level_wrappers.calculated_channels import ( CalculatedChannelsLowLevelClient, ) from sift_client.resources._base import ResourceBase -from sift_client.types.calculated_channel import ( +from sift_client.sift_types.calculated_channel import ( CalculatedChannel, CalculatedChannelUpdate, ) -from sift_client.types.channel import ChannelReference from sift_client.util import cel_utils as cel if TYPE_CHECKING: + import re + from sift_client.client import SiftClient + from sift_client.sift_types.channel import ChannelReference class CalculatedChannelsAPIAsync(ResourceBase): - """ - High-level API for interacting with calculated channels. + """High-level API for interacting with calculated channels. This class provides a Pythonic, notebook-friendly interface for interacting with the CalculatedChannelsAPI. It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. @@ -30,9 +30,8 @@ class CalculatedChannelsAPIAsync(ResourceBase): representation of a calculated channel using standard Python data structures and types. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the CalculatedChannelsAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the CalculatedChannelsAPI. Args: sift_client: The Sift client to use. @@ -49,8 +48,7 @@ async def get( client_key: str | None = None, organization_id: str | None = None, ) -> CalculatedChannel: - """ - Get a Calculated Channel. + """Get a Calculated Channel. Args: calculated_channel_id: The ID of the calculated channel. @@ -74,7 +72,7 @@ async def get( return self._apply_client_to_instance(calculated_channel) - async def list( + async def list_( self, *, name: str | None = None, @@ -97,9 +95,8 @@ async def list( order_by: str | None = None, limit: int | None = None, organization_id: str | None = None, - ) -> List[CalculatedChannel]: - """ - List calculated channels with optional filtering. + ) -> list[CalculatedChannel]: + """List calculated channels with optional filtering. Args: name: Exact name of the calculated channel. @@ -171,8 +168,7 @@ async def list( return self._apply_client_to_instances(calculated_channels) async def find(self, **kwargs) -> CalculatedChannel | None: - """ - Find a single calculated channel matching the given query. Takes the same arguments as `list` but handles checking for multiple matches. + """Find a single calculated channel matching the given query. Takes the same arguments as `list` but handles checking for multiple matches. Will raise an error if multiple calculated channels are found. Args: @@ -181,7 +177,7 @@ async def find(self, **kwargs) -> CalculatedChannel | None: Returns: The CalculatedChannel found or None. """ - calculated_channels = await self.list(**kwargs) + calculated_channels = await self.list_(**kwargs) if len(calculated_channels) > 1: raise ValueError( f"Multiple calculated channels found for query: {kwargs}. " @@ -196,17 +192,16 @@ async def create( *, name: str, expression: str, - channel_references: List[ChannelReference], + channel_references: list[ChannelReference], description: str = "", units: str | None = None, client_key: str | None = None, - asset_ids: List[str] | None = None, - tag_ids: List[str] | None = None, + asset_ids: list[str] | None = None, + tag_ids: list[str] | None = None, all_assets: bool = False, user_notes: str = "", ) -> CalculatedChannel: - """ - Create a calculated channel. + """Create a calculated channel. Args: name: The name of the calculated channel. @@ -257,8 +252,7 @@ async def update( update: CalculatedChannelUpdate | dict, user_notes: str | None = None, ) -> CalculatedChannel: - """ - Update a Calculated Channel. + """Update a Calculated Channel. Args: calculated_channel: The CalculatedChannel or id of the CalculatedChannel to update. @@ -289,11 +283,9 @@ async def update( return self._apply_client_to_instance(updated_calculated_channel) async def archive(self, *, calculated_channel: str | CalculatedChannel) -> None: - """ - Archive a Calculated Channel. - """ + """Archive a Calculated Channel.""" update = CalculatedChannelUpdate( - archived_date=datetime.now(), + archived_date=datetime.now(tz=timezone.utc), ) await self.update(calculated_channel=calculated_channel, update=update) @@ -314,9 +306,8 @@ async def list_versions( include_archived: bool = False, order_by: str | None = None, limit: int | None = None, - ) -> List[CalculatedChannel]: - """ - List versions of a calculated channel. + ) -> list[CalculatedChannel]: + """List versions of a calculated channel. Args: calculated_channel_id: The ID of the calculated channel. diff --git a/python/lib/sift_client/resources/channels.py b/python/lib/sift_client/resources/channels.py index 21d4e65bc..352677715 100644 --- a/python/lib/sift_client/resources/channels.py +++ b/python/lib/sift_client/resources/channels.py @@ -1,25 +1,26 @@ from __future__ import annotations import re -from datetime import datetime -from typing import TYPE_CHECKING, Dict, List +from typing import TYPE_CHECKING -import pandas as pd import pyarrow as pa from sift_client._internal.low_level_wrappers.channels import ChannelsLowLevelClient from sift_client._internal.low_level_wrappers.data import DataLowLevelClient from sift_client.resources._base import ResourceBase -from sift_client.types.channel import Channel from sift_client.util import cel_utils as cel if TYPE_CHECKING: + from datetime import datetime + + import pandas as pd + from sift_client.client import SiftClient + from sift_client.sift_types.channel import Channel class ChannelsAPIAsync(ResourceBase): - """ - High-level API for interacting with channels. + """High-level API for interacting with channels. This class provides a Pythonic, notebook-friendly interface for interacting with the ChannelsAPI. It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. @@ -28,9 +29,8 @@ class ChannelsAPIAsync(ResourceBase): representation of a channel using standard Python data structures and types. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the ChannelsAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the ChannelsAPI. Args: sift_client: The Sift client to use. @@ -44,8 +44,7 @@ async def get( *, channel_id: str, ) -> Channel: - """ - Get a Channel. + """Get a Channel. Args: channel_id: The ID of the channel. @@ -56,7 +55,7 @@ async def get( channel = await self._low_level_client.get_channel(channel_id=channel_id) return self._apply_client_to_instance(channel) - async def list( + async def list_( self, *, asset_id: str | None = None, @@ -76,8 +75,7 @@ async def list( order_by: str | None = None, limit: int | None = None, ) -> list[Channel]: - """ - List channels with optional filtering. + """List channels with optional filtering. Args: asset_id: The asset ID to get. @@ -151,8 +149,7 @@ async def list( return self._apply_client_to_instances(channels) async def find(self, **kwargs) -> Channel | None: - """ - Find a single channel matching the given query. Takes the same arguments as `list`. If more than one channel is found, + """Find a single channel matching the given query. Takes the same arguments as `list`. If more than one channel is found, raises an error. Args: @@ -161,7 +158,7 @@ async def find(self, **kwargs) -> Channel | None: Returns: The Channel found or None. """ - channels = await self.list(**kwargs) + channels = await self.list_(**kwargs) if len(channels) > 1: raise ValueError("Multiple channels found for query") elif len(channels) == 1: @@ -171,14 +168,13 @@ async def find(self, **kwargs) -> Channel | None: async def get_data( self, *, - channels: List[Channel], + channels: list[Channel], run_id: str | None = None, start_time: datetime | None = None, end_time: datetime | None = None, limit: int | None = None, - ) -> Dict[str, pd.DataFrame]: - """ - Get data for one or more channels. + ) -> dict[str, pd.DataFrame]: + """Get data for one or more channels. Args: channels: The channels to get data for. @@ -198,15 +194,13 @@ async def get_data( async def get_data_as_arrow( self, *, - channels: List[Channel], + channels: list[Channel], run_id: str | None = None, start_time: datetime | None = None, end_time: datetime | None = None, limit: int | None = None, - ) -> Dict[str, pa.Table]: - """ - Get data for one or more channels as pyarrow tables. - """ + ) -> dict[str, pa.Table]: + """Get data for one or more channels as pyarrow tables.""" data = await self.get_data( channels=channels, run_id=run_id, diff --git a/python/lib/sift_client/resources/ingestion.py b/python/lib/sift_client/resources/ingestion.py index 9830b3f02..8abd088ae 100644 --- a/python/lib/sift_client/resources/ingestion.py +++ b/python/lib/sift_client/resources/ingestion.py @@ -1,22 +1,22 @@ from __future__ import annotations import logging -from datetime import datetime -from typing import TYPE_CHECKING, Any, List +from typing import TYPE_CHECKING, Any from sift_client._internal.low_level_wrappers.ingestion import IngestionLowLevelClient from sift_client.resources._base import ResourceBase -from sift_client.types.ingestion import Flow if TYPE_CHECKING: + from datetime import datetime + from sift_client.client import SiftClient + from sift_client.sift_types.ingestion import Flow logger = logging.getLogger(__name__) class IngestionAPIAsync(ResourceBase): - """ - High-level API for interacting with ingestion services. + """High-level API for interacting with ingestion services. This class provides a Pythonic, notebook-friendly interface for interacting with the IngestionAPI. It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. @@ -25,9 +25,8 @@ class IngestionAPIAsync(ResourceBase): representation of ingestion flows using standard Python data structures and types. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the IngestionAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the IngestionAPI. Args: sift_client: The Sift client to use. @@ -40,12 +39,11 @@ async def create_ingestion_config( *, asset_name: str, run_id: str | None = None, - flows: List[Flow], + flows: list[Flow], client_key: str | None = None, organization_id: str | None = None, ) -> str: - """ - Create an ingestion config. + """Create an ingestion config. Args: asset_name: The name of the asset for this ingestion config. @@ -85,6 +83,13 @@ def ingest( timestamp: datetime, channel_values: dict[str, Any], ): + """Ingest data for a flow. + + Args: + flow: The flow to ingest data for. + timestamp: The timestamp of the data. + channel_values: Dictionary mapping channel names to their values. + """ self._low_level_client.ingest_flow( flow=flow, timestamp=timestamp, @@ -92,8 +97,7 @@ def ingest( ) def wait_for_ingestion_to_complete(self, timeout: float | None = None): - """ - Wait for all ingestion to complete. + """Wait for all ingestion to complete. Args: run_id: The id of the run to wait for. diff --git a/python/lib/sift_client/resources/ping.py b/python/lib/sift_client/resources/ping.py index e1b33c0f9..f1f2d89b2 100644 --- a/python/lib/sift_client/resources/ping.py +++ b/python/lib/sift_client/resources/ping.py @@ -10,13 +10,10 @@ class PingAPIAsync(ResourceBase): - """ - High-level API for performing health checks. - """ + """High-level API for performing health checks.""" - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the AssetsAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the AssetsAPI. Args: sift_client: The Sift client to use. @@ -25,8 +22,7 @@ def __init__(self, sift_client: "SiftClient"): self._low_level_client = PingLowLevelClient(sift_client.grpc_client) async def ping(self) -> str: - """ - Send a ping request to the server. + """Send a ping request to the server. Returns: The response from the server. diff --git a/python/lib/sift_client/resources/rules.py b/python/lib/sift_client/resources/rules.py index 6f4ab0132..a101a3ae5 100644 --- a/python/lib/sift_client/resources/rules.py +++ b/python/lib/sift_client/resources/rules.py @@ -1,21 +1,21 @@ from __future__ import annotations -import re -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from sift_client._internal.low_level_wrappers.rules import RulesLowLevelClient from sift_client.resources._base import ResourceBase -from sift_client.types.channel import ChannelReference -from sift_client.types.rule import Rule, RuleAction, RuleUpdate +from sift_client.sift_types.rule import Rule, RuleAction, RuleUpdate from sift_client.util import cel_utils as cel if TYPE_CHECKING: + import re + from sift_client.client import SiftClient + from sift_client.sift_types.channel import ChannelReference class RulesAPIAsync(ResourceBase): - """ - High-level API for interacting with rules. + """High-level API for interacting with rules. This class provides a Pythonic, notebook-friendly interface for interacting with the RulesAPI. It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. @@ -24,9 +24,8 @@ class RulesAPIAsync(ResourceBase): representation of a rule using standard Python data structures and types. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the RulesAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the RulesAPI. Args: sift_client: The Sift client to use. @@ -40,8 +39,7 @@ async def get( rule_id: str | None = None, client_key: str | None = None, ) -> Rule: - """ - Get a Rule. + """Get a Rule. Args: rule_id: The ID of the rule. @@ -53,7 +51,7 @@ async def get( rule = await self._low_level_client.get_rule(rule_id=rule_id, client_key=client_key) return self._apply_client_to_instance(rule) - async def list( + async def list_( self, *, name: str | None = None, @@ -63,8 +61,7 @@ async def list( limit: int | None = None, include_deleted: bool = False, ) -> list[Rule]: - """ - List rules with optional filtering. + """List rules with optional filtering. Args: name: Exact name of the rule. @@ -72,6 +69,7 @@ async def list( name_regex: Regular expression string to filter rules by name. order_by: How to order the retrieved rules. limit: How many rules to retrieve. If None, retrieves all matches. + include_deleted: Include deleted rules. Returns: A list of Rules that matches the filter. @@ -98,8 +96,7 @@ async def list( return self._apply_client_to_instances(rules) async def find(self, **kwargs) -> Rule | None: - """ - Find a single rule matching the given query. Takes the same arguments as `list`. If more than one rule is found, + """Find a single rule matching the given query. Takes the same arguments as `list`. If more than one rule is found, raises an error. Args: @@ -108,7 +105,7 @@ async def find(self, **kwargs) -> Rule | None: Returns: The Rule found or None. """ - rules = await self.list(**kwargs) + rules = await self.list_(**kwargs) if len(rules) > 1: raise ValueError("Multiple rules found for query") elif len(rules) == 1: @@ -120,17 +117,15 @@ async def create( name: str, description: str, expression: str, - channel_references: List[ChannelReference], + channel_references: list[ChannelReference], action: RuleAction, organization_id: str | None = None, client_key: str | None = None, - asset_ids: List[str] | None = None, - contextual_channels: List[str] | None = None, + asset_ids: list[str] | None = None, + contextual_channels: list[str] | None = None, is_external: bool = False, ) -> Rule: - """ - Create a new rule. - """ + """Create a new rule.""" created_rule = await self._low_level_client.create_rule( name=name, description=description, @@ -148,13 +143,13 @@ async def create( async def update( self, rule: str | Rule, update: RuleUpdate | dict, version_notes: str | None = None ) -> Rule: - """ - Update a Rule. + """Update a Rule. Args: rule: The Rule or rule ID to update. update: Updates to apply to the Rule. version_notes: Notes to include in the rule version. + Returns: The updated Rule. """ @@ -171,12 +166,11 @@ async def archive( self, *, rule: str | Rule | None = None, - rules: List[Rule] | None = None, - rule_ids: List[str] | None = None, - client_keys: List[str] | None = None, + rules: list[Rule] | None = None, + rule_ids: list[str] | None = None, + client_keys: list[str] | None = None, ) -> None: - """ - Archive a rule or multiple. + """Archive a rule or multiple. Args: rule: The Rule to archive. @@ -213,8 +207,7 @@ async def restore( rule_id: str | None = None, client_key: str | None = None, ) -> Rule: - """ - Restore a rule. + """Restore a rule. Args: rule: The Rule or rule ID to restore. @@ -237,11 +230,10 @@ async def restore( async def batch_restore( self, *, - rule_ids: List[str] | None = None, - client_keys: List[str] | None = None, + rule_ids: list[str] | None = None, + client_keys: list[str] | None = None, ) -> None: - """ - Batch restore rules. + """Batch restore rules. Args: rule_ids: List of rule IDs to restore. @@ -252,11 +244,10 @@ async def batch_restore( async def batch_get( self, *, - rule_ids: List[str] | None = None, - client_keys: List[str] | None = None, - ) -> List[Rule]: - """ - Get multiple rules by rule IDs or client keys. + rule_ids: list[str] | None = None, + client_keys: list[str] | None = None, + ) -> list[Rule]: + """Get multiple rules by rule IDs or client keys. Args: rule_ids: List of rule IDs to get. diff --git a/python/lib/sift_client/resources/runs.py b/python/lib/sift_client/resources/runs.py index 7ce01ac6b..abb324f89 100644 --- a/python/lib/sift_client/resources/runs.py +++ b/python/lib/sift_client/resources/runs.py @@ -1,21 +1,21 @@ from __future__ import annotations import re -from datetime import datetime -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from sift_client._internal.low_level_wrappers.runs import RunsLowLevelClient from sift_client.resources._base import ResourceBase -from sift_client.types.run import Run, RunUpdate +from sift_client.sift_types.run import Run, RunUpdate from sift_client.util.cel_utils import contains, equals, equals_null, match, not_ if TYPE_CHECKING: + from datetime import datetime + from sift_client.client import SiftClient class RunsAPIAsync(ResourceBase): - """ - High-level API for interacting with runs. + """High-level API for interacting with runs. This class provides a Pythonic, notebook-friendly interface for interacting with the RunsAPI. It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. @@ -24,9 +24,8 @@ class RunsAPIAsync(ResourceBase): representation of a run using standard Python data structures and types. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the RunsAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the RunsAPI. Args: sift_client: The Sift client to use. @@ -39,8 +38,7 @@ async def get( *, run_id: str, ) -> Run: - """ - Get a Run. + """Get a Run. Args: run_id: The ID of the run. @@ -51,7 +49,7 @@ async def get( run = await self._low_level_client.get_run(run_id=run_id) return self._apply_client_to_instance(run) - async def list( + async def list_( self, *, name: str | None = None, @@ -68,9 +66,8 @@ async def list( include_archived: bool = False, order_by: str | None = None, limit: int | None = None, - ) -> List[Run]: - """ - List runs with optional filtering. + ) -> list[Run]: + """List runs with optional filtering. Args: name: Exact name of the run. @@ -139,8 +136,7 @@ async def list( return self._apply_client_to_instances(runs) async def find(self, **kwargs) -> Run | None: - """ - Find a single run matching the given query. Takes the same arguments as `list`. If more than one run is found, + """Find a single run matching the given query. Takes the same arguments as `list`. If more than one run is found, raises an error. Args: @@ -149,7 +145,7 @@ async def find(self, **kwargs) -> Run | None: Returns: The Run found or None. """ - runs = await self.list(**kwargs) + runs = await self.list_(**kwargs) if len(runs) > 1: raise ValueError("Multiple runs found for query") elif len(runs) == 1: @@ -160,15 +156,14 @@ async def create( self, name: str, description: str, - tags: List[str] | None = None, + tags: list[str] | None = None, start_time: datetime | None = None, stop_time: datetime | None = None, organization_id: str | None = None, client_key: str | None = None, metadata: dict[str, str | float | bool] | None = None, ) -> Run: - """ - Create a new run. + """Create a new run. Args: name: The name of the run. @@ -196,8 +191,7 @@ async def create( return self._apply_client_to_instance(created_run) async def update(self, run: str | Run, update: RunUpdate | dict) -> Run: - """ - Update a Run. + """Update a Run. Args: run: The Run or run ID to update. @@ -221,8 +215,7 @@ async def archive( *, run: str | Run, ) -> None: - """ - Archive a run. + """Archive a run. Args: run: The Run or run ID to archive. @@ -237,8 +230,7 @@ async def stop( *, run: str | Run, ) -> None: - """ - Stop a run by setting its stop time to the current time. + """Stop a run by setting its stop time to the current time. Args: run: The Run or run ID to stop. @@ -249,10 +241,9 @@ async def stop( async def create_automatic_association_for_assets( self, run: str | Run, - asset_names: List[str], + asset_names: list[str], ) -> None: - """ - Associate assets with a run for automatic data ingestion. + """Associate assets with a run for automatic data ingestion. Args: run: The Run or run ID. @@ -264,8 +255,7 @@ async def create_automatic_association_for_assets( ) async def stop_run(self, run: str | Run) -> None: - """ - Stop a run by setting its stop time to the current time. + """Stop a run by setting its stop time to the current time. Args: run: The Run or run ID to stop. diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.py b/python/lib/sift_client/resources/sync_stubs/__init__.py index a18c1db31..7246389a4 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.py +++ b/python/lib/sift_client/resources/sync_stubs/__init__.py @@ -1,5 +1,4 @@ -""" -Synchronous API wrappers generated from async classes. +"""Synchronous API wrappers generated from async classes. This package contains synchronous versions of all async API classes. """ @@ -20,4 +19,4 @@ RulesAPI = generate_sync_api(RulesAPIAsync, "RulesAPI") RunsAPI = generate_sync_api(RunsAPIAsync, "RunsAPI") -__all__ = ["PingAPI", "AssetsAPI", "CalculatedChannelsAPI", "RunsAPI"] +__all__ = ["AssetsAPI", "CalculatedChannelsAPI", "PingAPI", "RunsAPI"] diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index a75e1d414..0c52d3b15 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -4,61 +4,54 @@ from __future__ import annotations import re from datetime import datetime -from typing import Any, Dict, List +from typing import Any import pandas as pd import pyarrow as pa from sift_client.client import SiftClient -from sift_client.types.asset import Asset, AssetUpdate -from sift_client.types.calculated_channel import CalculatedChannel, CalculatedChannelUpdate -from sift_client.types.channel import Channel, ChannelReference -from sift_client.types.rule import Rule, RuleAction, RuleUpdate -from sift_client.types.run import Run, RunUpdate +from sift_client.sift_types.asset import Asset, AssetUpdate +from sift_client.sift_types.calculated_channel import CalculatedChannel, CalculatedChannelUpdate +from sift_client.sift_types.channel import Channel, ChannelReference +from sift_client.sift_types.rule import Rule, RuleAction, RuleUpdate +from sift_client.sift_types.run import Run, RunUpdate class AssetsAPI: - """ - Sync counterpart to `AssetsAPIAsync`. - + """Sync counterpart to `AssetsAPIAsync`. High-level API for interacting with assets. - This class provides a Pythonic, notebook-friendly interface for interacting with the AssetsAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + This class provides a Pythonic, notebook-friendly interface for interacting with the AssetsAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + + All methods in this class use the Asset class from the low-level wrapper, which is a user-friendly + representation of an asset using standard Python data structures and types. - All methods in this class use the Asset class from the low-level wrapper, which is a user-friendly - representation of an asset using standard Python data structures and types. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the AssetsAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the AssetsAPI. Args: sift_client: The Sift client to use. """ ... - def _run(self, coro): - """ """ - ... - + def _run(self, coro): ... def archive(self, asset: str | Asset, *, archive_runs: bool = False) -> Asset: - """ - Archive an asset. + """Archive an asset. - Args: + Args: asset: The Asset or asset ID to archive. archive_runs: If True, archive all Runs associated with the Asset. - Returns: + Returns: The archived Asset. """ ... def find(self, **kwargs) -> Asset | None: - """ - Find a single asset matching the given query. Takes the same arguments as `list_`. If more than one asset is found, + """Find a single asset matching the given query. Takes the same arguments as `list_`. If more than one asset is found, raises an error. Args: @@ -70,8 +63,7 @@ class AssetsAPI: ... def get(self, *, asset_id: str | None = None, name: str | None = None) -> Asset: - """ - Get an Asset. + """Get an Asset. Args: asset_id: The ID of the asset. @@ -103,8 +95,7 @@ class AssetsAPI: order_by: str | None = None, limit: int | None = None, ) -> list[Asset]: - """ - List assets with optional filtering. + """List assets with optional filtering. Args: asset_ids: List of asset IDs to filter by. @@ -120,6 +111,7 @@ class AssetsAPI: modified_by: Assets last modified by this user. tags: Assets with these tags. tag_ids: List of asset tag IDs to filter by. + metadata: metadata filter include_archived: Include archived assets. filter_query: Explicit CEL query to filter assets. order_by: How to order the retrieved assets. # TODO: tooling for this? @@ -131,8 +123,7 @@ class AssetsAPI: ... def update(self, asset: str | Asset, update: AssetUpdate | dict) -> Asset: - """ - Update an Asset. + """Update an Asset. Args: asset: The Asset or asset ID to update. @@ -144,36 +135,29 @@ class AssetsAPI: ... class CalculatedChannelsAPI: - """ - Sync counterpart to `CalculatedChannelsAPIAsync`. - + """Sync counterpart to `CalculatedChannelsAPIAsync`. High-level API for interacting with calculated channels. - This class provides a Pythonic, notebook-friendly interface for interacting with the CalculatedChannelsAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + This class provides a Pythonic, notebook-friendly interface for interacting with the CalculatedChannelsAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + + All methods in this class use the CalculatedChannel class from the low-level wrapper, which is a user-friendly + representation of a calculated channel using standard Python data structures and types. - All methods in this class use the CalculatedChannel class from the low-level wrapper, which is a user-friendly - representation of a calculated channel using standard Python data structures and types. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the CalculatedChannelsAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the CalculatedChannelsAPI. Args: sift_client: The Sift client to use. """ ... - def _run(self, coro): - """ """ - ... - + def _run(self, coro): ... def archive(self, *, calculated_channel: str | CalculatedChannel) -> None: - """ - Archive a Calculated Channel. - """ + """Archive a Calculated Channel.""" ... def create( @@ -181,17 +165,16 @@ class CalculatedChannelsAPI: *, name: str, expression: str, - channel_references: List[ChannelReference], + channel_references: list[ChannelReference], description: str = "", units: str | None = None, client_key: str | None = None, - asset_ids: List[str] | None = None, - tag_ids: List[str] | None = None, + asset_ids: list[str] | None = None, + tag_ids: list[str] | None = None, all_assets: bool = False, user_notes: str = "", ) -> CalculatedChannel: - """ - Create a calculated channel. + """Create a calculated channel. Args: name: The name of the calculated channel. @@ -214,8 +197,7 @@ class CalculatedChannelsAPI: ... def find(self, **kwargs) -> CalculatedChannel | None: - """ - Find a single calculated channel matching the given query. Takes the same arguments as `list` but handles checking for multiple matches. + """Find a single calculated channel matching the given query. Takes the same arguments as `list` but handles checking for multiple matches. Will raise an error if multiple calculated channels are found. Args: @@ -233,8 +215,7 @@ class CalculatedChannelsAPI: client_key: str | None = None, organization_id: str | None = None, ) -> CalculatedChannel: - """ - Get a Calculated Channel. + """Get a Calculated Channel. Args: calculated_channel_id: The ID of the calculated channel. @@ -249,7 +230,7 @@ class CalculatedChannelsAPI: """ ... - def list( + def list_( self, *, name: str | None = None, @@ -272,9 +253,8 @@ class CalculatedChannelsAPI: order_by: str | None = None, limit: int | None = None, organization_id: str | None = None, - ) -> List[CalculatedChannel]: - """ - List calculated channels with optional filtering. + ) -> list[CalculatedChannel]: + """List calculated channels with optional filtering. Args: name: Exact name of the calculated channel. @@ -320,9 +300,8 @@ class CalculatedChannelsAPI: include_archived: bool = False, order_by: str | None = None, limit: int | None = None, - ) -> List[CalculatedChannel]: - """ - List versions of a calculated channel. + ) -> list[CalculatedChannel]: + """List versions of a calculated channel. Args: calculated_channel_id: The ID of the calculated channel. @@ -355,8 +334,7 @@ class CalculatedChannelsAPI: update: CalculatedChannelUpdate | dict, user_notes: str | None = None, ) -> CalculatedChannel: - """ - Update a Calculated Channel. + """Update a Calculated Channel. Args: calculated_channel: The CalculatedChannel or id of the CalculatedChannel to update. @@ -369,35 +347,29 @@ class CalculatedChannelsAPI: ... class ChannelsAPI: - """ - Sync counterpart to `ChannelsAPIAsync`. - + """Sync counterpart to `ChannelsAPIAsync`. High-level API for interacting with channels. - This class provides a Pythonic, notebook-friendly interface for interacting with the ChannelsAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + This class provides a Pythonic, notebook-friendly interface for interacting with the ChannelsAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + + All methods in this class use the Channel class from the low-level wrapper, which is a user-friendly + representation of a channel using standard Python data structures and types. - All methods in this class use the Channel class from the low-level wrapper, which is a user-friendly - representation of a channel using standard Python data structures and types. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the ChannelsAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the ChannelsAPI. Args: sift_client: The Sift client to use. """ ... - def _run(self, coro): - """ """ - ... - + def _run(self, coro): ... def find(self, **kwargs) -> Channel | None: - """ - Find a single channel matching the given query. Takes the same arguments as `list`. If more than one channel is found, + """Find a single channel matching the given query. Takes the same arguments as `list`. If more than one channel is found, raises an error. Args: @@ -409,8 +381,7 @@ class ChannelsAPI: ... def get(self, *, channel_id: str) -> Channel: - """ - Get a Channel. + """Get a Channel. Args: channel_id: The ID of the channel. @@ -423,14 +394,13 @@ class ChannelsAPI: def get_data( self, *, - channels: List[Channel], + channels: list[Channel], run_id: str | None = None, start_time: datetime | None = None, end_time: datetime | None = None, limit: int | None = None, - ) -> Dict[str, pd.DataFrame]: - """ - Get data for one or more channels. + ) -> dict[str, pd.DataFrame]: + """Get data for one or more channels. Args: channels: The channels to get data for. @@ -444,18 +414,16 @@ class ChannelsAPI: def get_data_as_arrow( self, *, - channels: List[Channel], + channels: list[Channel], run_id: str | None = None, start_time: datetime | None = None, end_time: datetime | None = None, limit: int | None = None, - ) -> Dict[str, pa.Table]: - """ - Get data for one or more channels as pyarrow tables. - """ + ) -> dict[str, pa.Table]: + """Get data for one or more channels as pyarrow tables.""" ... - def list( + def list_( self, *, asset_id: str | None = None, @@ -475,8 +443,7 @@ class ChannelsAPI: order_by: str | None = None, limit: int | None = None, ) -> list[Channel]: - """ - List channels with optional filtering. + """List channels with optional filtering. Args: asset_id: The asset ID to get. @@ -502,29 +469,22 @@ class ChannelsAPI: ... class PingAPI: - """ - Sync counterpart to `PingAPIAsync`. - + """Sync counterpart to `PingAPIAsync`. High-level API for performing health checks. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the AssetsAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the AssetsAPI. Args: sift_client: The Sift client to use. """ ... - def _run(self, coro): - """ """ - ... - + def _run(self, coro): ... def ping(self) -> str: - """ - Send a ping request to the server. + """Send a ping request to the server. Returns: The response from the server. @@ -532,42 +492,36 @@ class PingAPI: ... class RulesAPI: - """ - Sync counterpart to `RulesAPIAsync`. - + """Sync counterpart to `RulesAPIAsync`. High-level API for interacting with rules. - This class provides a Pythonic, notebook-friendly interface for interacting with the RulesAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + This class provides a Pythonic, notebook-friendly interface for interacting with the RulesAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + + All methods in this class use the Rule class from the low-level wrapper, which is a user-friendly + representation of a rule using standard Python data structures and types. - All methods in this class use the Rule class from the low-level wrapper, which is a user-friendly - representation of a rule using standard Python data structures and types. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the RulesAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the RulesAPI. Args: sift_client: The Sift client to use. """ ... - def _run(self, coro): - """ """ - ... - + def _run(self, coro): ... def archive( self, *, rule: str | Rule | None = None, - rules: List[Rule] | None = None, - rule_ids: List[str] | None = None, - client_keys: List[str] | None = None, + rules: list[Rule] | None = None, + rule_ids: list[str] | None = None, + client_keys: list[str] | None = None, ) -> None: - """ - Archive a rule or multiple. + """Archive a rule or multiple. Args: rule: The Rule to archive. @@ -578,10 +532,9 @@ class RulesAPI: ... def batch_get( - self, *, rule_ids: List[str] | None = None, client_keys: List[str] | None = None - ) -> List[Rule]: - """ - Get multiple rules by rule IDs or client keys. + self, *, rule_ids: list[str] | None = None, client_keys: list[str] | None = None + ) -> list[Rule]: + """Get multiple rules by rule IDs or client keys. Args: rule_ids: List of rule IDs to get. @@ -593,10 +546,9 @@ class RulesAPI: ... def batch_restore( - self, *, rule_ids: List[str] | None = None, client_keys: List[str] | None = None + self, *, rule_ids: list[str] | None = None, client_keys: list[str] | None = None ) -> None: - """ - Batch restore rules. + """Batch restore rules. Args: rule_ids: List of rule IDs to restore. @@ -609,22 +561,19 @@ class RulesAPI: name: str, description: str, expression: str, - channel_references: List[ChannelReference], + channel_references: list[ChannelReference], action: RuleAction, organization_id: str | None = None, client_key: str | None = None, - asset_ids: List[str] | None = None, - contextual_channels: List[str] | None = None, + asset_ids: list[str] | None = None, + contextual_channels: list[str] | None = None, is_external: bool = False, ) -> Rule: - """ - Create a new rule. - """ + """Create a new rule.""" ... def find(self, **kwargs) -> Rule | None: - """ - Find a single rule matching the given query. Takes the same arguments as `list`. If more than one rule is found, + """Find a single rule matching the given query. Takes the same arguments as `list`. If more than one rule is found, raises an error. Args: @@ -636,8 +585,7 @@ class RulesAPI: ... def get(self, *, rule_id: str | None = None, client_key: str | None = None) -> Rule: - """ - Get a Rule. + """Get a Rule. Args: rule_id: The ID of the rule. @@ -648,7 +596,7 @@ class RulesAPI: """ ... - def list( + def list_( self, *, name: str | None = None, @@ -658,8 +606,7 @@ class RulesAPI: limit: int | None = None, include_deleted: bool = False, ) -> list[Rule]: - """ - List rules with optional filtering. + """List rules with optional filtering. Args: name: Exact name of the rule. @@ -667,6 +614,7 @@ class RulesAPI: name_regex: Regular expression string to filter rules by name. order_by: How to order the retrieved rules. limit: How many rules to retrieve. If None, retrieves all matches. + include_deleted: Include deleted rules. Returns: A list of Rules that matches the filter. @@ -676,8 +624,7 @@ class RulesAPI: def restore( self, *, rule: str | Rule, rule_id: str | None = None, client_key: str | None = None ) -> Rule: - """ - Restore a rule. + """Restore a rule. Args: rule: The Rule or rule ID to restore. @@ -692,48 +639,42 @@ class RulesAPI: def update( self, rule: str | Rule, update: RuleUpdate | dict, version_notes: str | None = None ) -> Rule: - """ - Update a Rule. + """Update a Rule. Args: rule: The Rule or rule ID to update. update: Updates to apply to the Rule. version_notes: Notes to include in the rule version. + Returns: The updated Rule. """ ... class RunsAPI: - """ - Sync counterpart to `RunsAPIAsync`. - + """Sync counterpart to `RunsAPIAsync`. High-level API for interacting with runs. - This class provides a Pythonic, notebook-friendly interface for interacting with the RunsAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + This class provides a Pythonic, notebook-friendly interface for interacting with the RunsAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + + All methods in this class use the Run class from the low-level wrapper, which is a user-friendly + representation of a run using standard Python data structures and types. - All methods in this class use the Run class from the low-level wrapper, which is a user-friendly - representation of a run using standard Python data structures and types. """ - def __init__(self, sift_client: "SiftClient"): - """ - Initialize the RunsAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the RunsAPI. Args: sift_client: The Sift client to use. """ ... - def _run(self, coro): - """ """ - ... - + def _run(self, coro): ... def archive(self, *, run: str | Run) -> None: - """ - Archive a run. + """Archive a run. Args: run: The Run or run ID to archive. @@ -744,15 +685,14 @@ class RunsAPI: self, name: str, description: str, - tags: List[str] | None = None, + tags: list[str] | None = None, start_time: datetime | None = None, stop_time: datetime | None = None, organization_id: str | None = None, client_key: str | None = None, metadata: dict[str, str | float | bool] | None = None, ) -> Run: - """ - Create a new run. + """Create a new run. Args: name: The name of the run. @@ -770,10 +710,9 @@ class RunsAPI: ... def create_automatic_association_for_assets( - self, run: str | Run, asset_names: List[str] + self, run: str | Run, asset_names: list[str] ) -> None: - """ - Associate assets with a run for automatic data ingestion. + """Associate assets with a run for automatic data ingestion. Args: run: The Run or run ID. @@ -782,8 +721,7 @@ class RunsAPI: ... def find(self, **kwargs) -> Run | None: - """ - Find a single run matching the given query. Takes the same arguments as `list`. If more than one run is found, + """Find a single run matching the given query. Takes the same arguments as `list`. If more than one run is found, raises an error. Args: @@ -795,8 +733,7 @@ class RunsAPI: ... def get(self, *, run_id: str) -> Run: - """ - Get a Run. + """Get a Run. Args: run_id: The ID of the run. @@ -806,7 +743,7 @@ class RunsAPI: """ ... - def list( + def list_( self, *, name: str | None = None, @@ -823,9 +760,8 @@ class RunsAPI: include_archived: bool = False, order_by: str | None = None, limit: int | None = None, - ) -> List[Run]: - """ - List runs with optional filtering. + ) -> list[Run]: + """List runs with optional filtering. Args: name: Exact name of the run. @@ -849,8 +785,7 @@ class RunsAPI: ... def stop(self, *, run: str | Run) -> None: - """ - Stop a run by setting its stop time to the current time. + """Stop a run by setting its stop time to the current time. Args: run: The Run or run ID to stop. @@ -858,8 +793,7 @@ class RunsAPI: ... def stop_run(self, run: str | Run) -> None: - """ - Stop a run by setting its stop time to the current time. + """Stop a run by setting its stop time to the current time. Args: run: The Run or run ID to stop. @@ -867,8 +801,7 @@ class RunsAPI: ... def update(self, run: str | Run, update: RunUpdate | dict) -> Run: - """ - Update a Run. + """Update a Run. Args: run: The Run or run ID to update. diff --git a/python/lib/sift_client/types/__init__.py b/python/lib/sift_client/sift_types/__init__.py similarity index 65% rename from python/lib/sift_client/types/__init__.py rename to python/lib/sift_client/sift_types/__init__.py index 6698dd0cb..6a389fa51 100644 --- a/python/lib/sift_client/types/__init__.py +++ b/python/lib/sift_client/sift_types/__init__.py @@ -1,16 +1,16 @@ -from sift_client.types.asset import Asset, AssetUpdate -from sift_client.types.calculated_channel import ( +from sift_client.sift_types.asset import Asset, AssetUpdate +from sift_client.sift_types.calculated_channel import ( CalculatedChannel, CalculatedChannelUpdate, ) -from sift_client.types.channel import ( +from sift_client.sift_types.channel import ( Channel, ChannelBitFieldElement, ChannelDataType, ChannelReference, ) -from sift_client.types.ingestion import IngestionConfig -from sift_client.types.rule import ( +from sift_client.sift_types.ingestion import IngestionConfig +from sift_client.sift_types.rule import ( Rule, RuleAction, RuleActionType, @@ -18,24 +18,24 @@ RuleUpdate, RuleVersion, ) -from sift_client.types.run import Run, RunUpdate +from sift_client.sift_types.run import Run, RunUpdate __all__ = [ "Asset", "AssetUpdate", "CalculatedChannel", "CalculatedChannelUpdate", - "Rule", - "RuleUpdate", - "RuleAction", - "RuleVersion", - "RuleActionType", - "RuleAnnotationType", "Channel", "ChannelBitFieldElement", "ChannelDataType", "ChannelReference", + "IngestionConfig", + "Rule", + "RuleAction", + "RuleActionType", + "RuleAnnotationType", + "RuleUpdate", + "RuleVersion", "Run", "RunUpdate", - "IngestionConfig", ] diff --git a/python/lib/sift_client/types/_base.py b/python/lib/sift_client/sift_types/_base.py similarity index 93% rename from python/lib/sift_client/types/_base.py rename to python/lib/sift_client/sift_types/_base.py index ab19ff278..9254992cf 100644 --- a/python/lib/sift_client/types/_base.py +++ b/python/lib/sift_client/sift_types/_base.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Callable, Generic, Optional, Type, TypeVar +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar from google.protobuf import field_mask_pb2, message from pydantic import BaseModel, ConfigDict, PrivateAttr @@ -37,7 +37,7 @@ def _apply_client_to_instance(self, client: SiftClient) -> None: self.__dict__["_client"] = client def _update(self, other: BaseType[ProtoT, SelfT]) -> BaseType[ProtoT, SelfT]: - """Update this instance with the values from another instance""" + """Update this instance with the values from another instance.""" # This bypasses the frozen status of the model for key in other.__class__.model_fields.keys(): if key in self.model_fields: @@ -55,17 +55,17 @@ class MappingHelper(BaseModel): proto_attr_path: str update_field: str | None = None - converter: Type[Any] | Callable[[Any], Any] | None = None + converter: type[Any] | Callable[[Any], Any] | None = None # TODO: how to handle nulling fields, needs to be default value for the type class ModelUpdate(BaseModel, Generic[ProtoT], ABC): - """Base class for Pydantic models that generate proto patches with field masks""" + """Base class for Pydantic models that generate proto patches with field masks.""" model_config = ConfigDict(frozen=False) - _resource_id: Optional[Any] = PrivateAttr(default=None) - _to_proto_helpers: dict[str, MappingHelper] = PrivateAttr(default={}) + _resource_id: Any | None = PrivateAttr(default=None) + _to_proto_helpers: ClassVar[dict[str, MappingHelper]] = PrivateAttr(default={}) def __init__(self, **data: Any): super().__init__(**data) @@ -86,9 +86,9 @@ def resource_id(self, value): self._resource_id = value def to_proto_with_mask(self) -> tuple[ProtoT, field_mask_pb2.FieldMask]: - """Convert to proto with field mask""" + """Convert to proto with field mask.""" # Get the corresponding proto class - proto_cls: Type[ProtoT] = self._get_proto_class() + proto_cls: type[ProtoT] = self._get_proto_class() proto_msg = proto_cls() # Get only explicitly set fields, including those set to None @@ -169,17 +169,17 @@ def _build_proto_and_paths( try: setattr(proto_msg, field_name, value) paths.append(path) - except TypeError: + except TypeError as e: raise TypeError( f"Can't set {field_name} to {value} on {proto_msg.__class__.__name__}" - ) + ) from e return paths - def _get_proto_class(self) -> Type[ProtoT]: + def _get_proto_class(self) -> type[ProtoT]: """Get the corresponding proto class - override in subclasses since typing is not strict.""" raise NotImplementedError("Subclasses must implement this") def _add_resource_id_to_proto(self, proto_msg: ProtoT): - """Assigns a resource ID (such as Asset ID) to the proto message""" + """Assigns a resource ID (such as Asset ID) to the proto message.""" raise NotImplementedError("Subclasses must implement this") diff --git a/python/lib/sift_client/types/asset.py b/python/lib/sift_client/sift_types/asset.py similarity index 69% rename from python/lib/sift_client/types/asset.py rename to python/lib/sift_client/sift_types/asset.py index bfcd68126..ad078a9ee 100644 --- a/python/lib/sift_client/types/asset.py +++ b/python/lib/sift_client/sift_types/asset.py @@ -1,23 +1,21 @@ from __future__ import annotations -from datetime import datetime -from typing import TYPE_CHECKING, List, Type +from datetime import datetime, timezone +from typing import TYPE_CHECKING, ClassVar from sift.assets.v1.assets_pb2 import Asset as AssetProto -from sift_client.types._base import BaseType, MappingHelper, ModelUpdate -from sift_client.types.channel import Channel -from sift_client.types.run import Run +from sift_client.sift_types._base import BaseType, MappingHelper, ModelUpdate from sift_client.util.metadata import metadata_dict_to_proto, metadata_proto_to_dict if TYPE_CHECKING: from sift_client.client import SiftClient + from sift_client.sift_types.channel import Channel + from sift_client.sift_types.run import Run class Asset(BaseType[AssetProto, "Asset"]): - """ - Model of the Sift Asset. - """ + """Model of the Sift Asset.""" name: str organization_id: str @@ -33,32 +31,37 @@ class Asset(BaseType[AssetProto, "Asset"]): def is_archived(self): """Whether the asset is archived.""" # TODO: clean up this logic when gRPC returns a null. - return self.archived_date is not None and self.archived_date > datetime(1970, 1, 1) + return self.archived_date is not None and self.archived_date > datetime( + 1970, 1, 1, tzinfo=timezone.utc + ) @property def created_by(self): + """Get the user that created this asset.""" raise NotImplementedError @property def modified_by(self): + """Get the user that modified this asset.""" raise NotImplementedError @property - def runs(self) -> List[Run]: - return self.client.runs.list(asset_id=self.id_) + def runs(self) -> list[Run]: + """Get the runs associated with this asset.""" + return self.client.runs.list_(asset_id=self.id_) - def channels(self, run_id: str | None = None, limit: int | None = None) -> List[Channel]: - """ - Return all channels for this asset. - """ - return self.client.channels.list(asset_id=self.id_, run_id=run_id, limit=limit) + def channels(self, run_id: str | None = None, limit: int | None = None) -> list[Channel]: + """Get the channels for this asset.""" + return self.client.channels.list_(asset_id=self.id_, run_id=run_id, limit=limit) @property def rules(self): + """Get the rules that apply to this asset.""" raise NotImplementedError @property def annotations(self): + """Get the annotations for this asset.""" raise NotImplementedError def archive(self, *, archive_runs: bool = False) -> Asset: @@ -72,8 +75,7 @@ def archive(self, *, archive_runs: bool = False) -> Asset: return self def update(self, update: AssetUpdate | dict) -> Asset: - """ - Update the Asset. + """Update the Asset. Args: update: Either an AssetUpdate instance or a dictionary of key-value pairs to update. @@ -89,33 +91,31 @@ def _from_proto(cls, proto: AssetProto, sift_client: SiftClient | None = None) - id_=proto.asset_id, name=proto.name, organization_id=proto.organization_id, - created_date=proto.created_date.ToDatetime(), + created_date=proto.created_date.ToDatetime(tzinfo=timezone.utc), created_by_user_id=proto.created_by_user_id, - modified_date=proto.modified_date.ToDatetime(), + modified_date=proto.modified_date.ToDatetime(tzinfo=timezone.utc), modified_by_user_id=proto.modified_by_user_id, tags=list(proto.tags) if proto.tags else [], - archived_date=proto.archived_date.ToDatetime(), + archived_date=proto.archived_date.ToDatetime(tzinfo=timezone.utc), metadata=metadata_proto_to_dict(proto.metadata), # type: ignore _client=sift_client, ) class AssetUpdate(ModelUpdate[AssetProto]): - """ - Model of the Asset Fields that can be updated. - """ + """Model of the Asset Fields that can be updated.""" tags: list[str] | None = None archived_date: datetime | str | None = None metadata: dict[str, str | float | bool] | None = None - _to_proto_helpers = { + _to_proto_helpers: ClassVar = { "metadata": MappingHelper( proto_attr_path="metadata", update_field="metadata", converter=metadata_dict_to_proto ), } - def _get_proto_class(self) -> Type[AssetProto]: + def _get_proto_class(self) -> type[AssetProto]: return AssetProto def _add_resource_id_to_proto(self, proto_msg: AssetProto): diff --git a/python/lib/sift_client/types/calculated_channel.py b/python/lib/sift_client/sift_types/calculated_channel.py similarity index 80% rename from python/lib/sift_client/types/calculated_channel.py rename to python/lib/sift_client/sift_types/calculated_channel.py index 3f64046cd..4f2bd8b71 100644 --- a/python/lib/sift_client/types/calculated_channel.py +++ b/python/lib/sift_client/sift_types/calculated_channel.py @@ -1,7 +1,7 @@ from __future__ import annotations -from datetime import datetime -from typing import TYPE_CHECKING, Any, Type +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any, ClassVar from sift.calculated_channels.v2.calculated_channels_pb2 import ( CalculatedChannel as CalculatedChannelProto, @@ -10,17 +10,15 @@ CalculatedChannelAbstractChannelReference, ) -from sift_client.types._base import BaseType, MappingHelper, ModelUpdate -from sift_client.types.channel import ChannelReference +from sift_client.sift_types._base import BaseType, MappingHelper, ModelUpdate +from sift_client.sift_types.channel import ChannelReference if TYPE_CHECKING: from sift_client.client import SiftClient class CalculatedChannel(BaseType[CalculatedChannelProto, "CalculatedChannel"]): - """ - Model of the Sift Calculated Channel. - """ + """Model of the Sift Calculated Channel.""" name: str description: str @@ -46,14 +44,18 @@ class CalculatedChannel(BaseType[CalculatedChannelProto, "CalculatedChannel"]): @property def is_archived(self): """Whether the calculated channel is archived.""" - return self.archived_date is not None and self.archived_date > datetime(1970, 1, 1) + return self.archived_date is not None and self.archived_date > datetime( + 1970, 1, 1, tzinfo=timezone.utc + ) @property def created_by(self): + """Get the user that created this calculated channel.""" raise NotImplementedError @property def modified_by(self): + """Get the user that modified this calculated channel.""" raise NotImplementedError def archive(self) -> CalculatedChannel: @@ -66,8 +68,7 @@ def update( update: CalculatedChannelUpdate | dict, user_notes: str | None = None, ) -> CalculatedChannel: - """ - Update the Calculated Channel. + """Update the Calculated Channel. Args: update: The update to apply to the calculated channel. See CalculatedChannelUpdate for more updatable fields. @@ -101,7 +102,9 @@ def _from_proto( organization_id=proto.organization_id, client_key=proto.client_key, archived_date=( - proto.archived_date.ToDatetime() if proto.HasField("archived_date") else None + proto.archived_date.ToDatetime(tzinfo=timezone.utc) + if proto.HasField("archived_date") + else None ), version_id=proto.version_id, version=proto.version, @@ -111,8 +114,8 @@ def _from_proto( asset_ids=proto.calculated_channel_configuration.asset_configuration.selection.asset_ids, # type: ignore tag_ids=proto.calculated_channel_configuration.asset_configuration.selection.tag_ids, # type: ignore all_assets=proto.calculated_channel_configuration.asset_configuration.all_assets, - created_date=proto.created_date.ToDatetime(), - modified_date=proto.modified_date.ToDatetime(), + created_date=proto.created_date.ToDatetime(tzinfo=timezone.utc), + modified_date=proto.modified_date.ToDatetime(tzinfo=timezone.utc), created_by_user_id=proto.created_by_user_id, modified_by_user_id=proto.modified_by_user_id, _client=sift_client, @@ -120,9 +123,7 @@ def _from_proto( class CalculatedChannelUpdate(ModelUpdate[CalculatedChannelProto]): - """ - Model of the Calculated Channel Fields that can be updated. - """ + """Model of the Calculated Channel Fields that can be updated.""" name: str | None = None description: str | None = None @@ -133,7 +134,7 @@ class CalculatedChannelUpdate(ModelUpdate[CalculatedChannelProto]): tag_ids: list[str] | None = None archived_date: datetime | None = None - _to_proto_helpers = { + _to_proto_helpers: ClassVar = { "expression": MappingHelper( proto_attr_path="calculated_channel_configuration.query_configuration.sel.expression", update_field="query_configuration", @@ -150,13 +151,22 @@ class CalculatedChannelUpdate(ModelUpdate[CalculatedChannelProto]): } def __init__(self, **data: Any): + """Initialize a CalculatedChannelUpdate instance. + + Args: + **data: Keyword arguments for the update fields. + + Raises: + ValueError: If only one of expression or expression_channel_references is provided. + Both must be provided together or neither should be provided. + """ super().__init__(**data) if any([self.expression, self.expression_channel_references]) and not all( [self.expression, self.expression_channel_references] ): raise ValueError("Expression and channel references must be set together") - def _get_proto_class(self) -> Type[CalculatedChannelProto]: + def _get_proto_class(self) -> type[CalculatedChannelProto]: return CalculatedChannelProto def _add_resource_id_to_proto(self, proto_msg: CalculatedChannelProto): diff --git a/python/lib/sift_client/types/channel.py b/python/lib/sift_client/sift_types/channel.py similarity index 80% rename from python/lib/sift_client/types/channel.py rename to python/lib/sift_client/sift_types/channel.py index 8f8aded44..9d91afe0c 100644 --- a/python/lib/sift_client/types/channel.py +++ b/python/lib/sift_client/sift_types/channel.py @@ -1,11 +1,11 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING import sift.common.type.v1.channel_data_type_pb2 as channel_pb -from pydantic import BaseModel +from pydantic import BaseModel, Field from sift.channels.v3.channels_pb2 import Channel as ChannelProto from sift.common.type.v1.channel_bit_field_element_pb2 import ( ChannelBitFieldElement as ChannelBitFieldElementPb, @@ -26,16 +26,17 @@ ) from sift.ingestion_configs.v2.ingestion_configs_pb2 import ChannelConfig -from sift_client.types._base import BaseType -from sift_client.types.run import Run +from sift_client.sift_types._base import BaseType if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.types.asset import Asset + from sift_client.sift_types.asset import Asset + from sift_client.sift_types.run import Run -# Enum for channel data types (mimics protobuf values, but as int for now) class ChannelDataType(Enum): + """Enum for channel data types (mimics protobuf values, but as int for now).""" + DOUBLE = channel_pb.CHANNEL_DATA_TYPE_DOUBLE STRING = channel_pb.CHANNEL_DATA_TYPE_STRING ENUM = channel_pb.CHANNEL_DATA_TYPE_ENUM @@ -55,14 +56,33 @@ def __str__(self) -> str: return ret @staticmethod - def from_api_format(val: str) -> Optional["ChannelDataType"]: + def from_api_format(val: str) -> ChannelDataType | None: + """Convert API format string to ChannelDataType. + + Args: + val: API format string representation of ChannelDataType. + + Returns: + ChannelDataType if conversion is successful, None otherwise. + """ for item in ChannelDataType: if "CHANNEL_DATA_TYPE_" + item.name == val: return item return None @staticmethod - def from_str(raw: str) -> Optional["ChannelDataType"]: + def from_str(raw: str) -> ChannelDataType | None: + """Convert string representation to ChannelDataType. + + Args: + raw: String representation of ChannelDataType. + + Returns: + ChannelDataType if conversion is successful, None otherwise. + + Raises: + Exception: If the string format is recognized but cannot be converted. + """ if raw.startswith("CHANNEL_DATA_TYPE_"): val = ChannelDataType.from_api_format(raw) if val is None: @@ -92,7 +112,18 @@ def from_str(raw: str) -> Optional["ChannelDataType"]: raise Exception(f"Unknown channel data type: {raw}") @staticmethod - def proto_data_class(data_type: ChannelDataType) -> Any: + def proto_data_class(data_type: ChannelDataType): + """Return the appropriate protobuf class for the given channel data type. + + Args: + data_type: The channel data type. + + Returns: + The protobuf class corresponding to the data type. + + Raises: + ValueError: If the data type is not recognized. + """ if data_type == ChannelDataType.DOUBLE: return DoubleValues elif data_type == ChannelDataType.FLOAT: @@ -120,6 +151,7 @@ def proto_data_class(data_type: ChannelDataType) -> Any: # TODO: Can we get rid of this? Is hashing the same between clients that likely to ever actually discover a conflict? def hash_str(self, api_format: bool = False) -> str: + """Get the hash string for this channel data type.""" if self == ChannelDataType.DOUBLE: return "CHANNEL_DATA_TYPE_DOUBLE" if api_format else ChannelDataType.DOUBLE.__str__() elif self == ChannelDataType.STRING: @@ -148,8 +180,9 @@ def hash_str(self, api_format: bool = False) -> str: raise Exception("Unreachable.") -# Bit field element model class ChannelBitFieldElement(BaseModel): + """Bit field element model.""" + name: str index: int bit_count: int @@ -172,12 +205,14 @@ def _to_proto(self) -> ChannelBitFieldElementPb: # Channel config model class Channel(BaseType[ChannelProto, "Channel"]): + """Model representing a Sift Channel.""" + name: str data_type: ChannelDataType description: str | None = None unit: str | None = None - bit_field_elements: List[ChannelBitFieldElement] | None = None - enum_types: Dict[str, int] = {} + bit_field_elements: list[ChannelBitFieldElement] = Field(default_factory=list) + enum_types: dict[str, int] = Field(default_factory=dict) asset_id: str | None = None created_date: datetime | None = None modified_date: datetime | None = None @@ -185,12 +220,13 @@ class Channel(BaseType[ChannelProto, "Channel"]): modified_by_user_id: str | None = None @staticmethod - def _enum_types_to_proto_list(enum_types: Dict[str, int]) -> List[ChannelEnumTypePb]: + def _enum_types_to_proto_list(enum_types: dict[str, int] | None) -> list[ChannelEnumTypePb]: """Convert a dictionary of enum types to a list of ChannelEnumTypePb objects.""" + enum_types = {} if enum_types is None else enum_types return [ChannelEnumTypePb(name=name, key=key) for name, key in enum_types.items()] @staticmethod - def _enum_types_from_proto_list(enum_types: List[ChannelEnumTypePb]) -> Dict[str, int]: + def _enum_types_from_proto_list(enum_types: list[ChannelEnumTypePb]) -> dict[str, int]: """Convert a list of ChannelEnumTypePb objects to a dictionary of enum types.""" return {enum.name: enum.key for enum in enum_types} @@ -210,8 +246,8 @@ def _from_proto( ], enum_types=cls._enum_types_from_proto_list(proto.enum_types), # type: ignore asset_id=proto.asset_id, - created_date=proto.created_date.ToDatetime(), - modified_date=proto.modified_date.ToDatetime(), + created_date=proto.created_date.ToDatetime(tzinfo=timezone.utc), + modified_date=proto.modified_date.ToDatetime(tzinfo=timezone.utc), created_by_user_id=proto.created_by_user_id, modified_by_user_id=proto.modified_by_user_id, _client=sift_client, @@ -245,14 +281,14 @@ def data( limit: int | None = None, as_arrow: bool = False, ): - """ - Retrieve channel data for this channel during the specified run. + """Retrieve channel data for this channel during the specified run. Args: run_id: The run ID to get data for. start_time: The start time to get data for. end_time: The end time to get data for. limit: The maximum number of data points to return. + as_arrow: Whether to return the data as an Arrow table. Returns: A dict of channel name to pandas DataFrame or Arrow Table object. @@ -277,17 +313,18 @@ def data( @property def asset(self) -> Asset: + """Get the asset that this channel belongs to.""" return self.client.assets.get(asset_id=self.asset_id) + # TODO: update this logic to correctly scope to only runs that this channel is associated with. @property - def runs(self) -> List[Run]: + def runs(self) -> list[Run]: + """Get all runs associated with this channel's asset.""" return self.asset.runs class ChannelReference(BaseModel): - """ - Channel reference for calculated channel or rule. - """ + """Channel reference for calculated channel or rule.""" channel_reference: str # The key of the channel in the expression i.e. $1, $2, etc. channel_identifier: str # The name of the channel diff --git a/python/lib/sift_client/types/ingestion.py b/python/lib/sift_client/sift_types/ingestion.py similarity index 87% rename from python/lib/sift_client/types/ingestion.py rename to python/lib/sift_client/sift_types/ingestion.py index 7c744651f..b449bf228 100644 --- a/python/lib/sift_client/types/ingestion.py +++ b/python/lib/sift_client/sift_types/ingestion.py @@ -1,8 +1,7 @@ from __future__ import annotations import math -from datetime import datetime -from typing import TYPE_CHECKING, Any, List +from typing import TYPE_CHECKING, Any from google.protobuf.empty_pb2 import Empty from pydantic import ConfigDict @@ -22,17 +21,17 @@ IngestWithConfigDataChannelValuePy, ) -from sift_client.types._base import BaseType -from sift_client.types.channel import Channel, ChannelDataType +from sift_client.sift_types._base import BaseType +from sift_client.sift_types.channel import Channel, ChannelDataType if TYPE_CHECKING: + from datetime import datetime + from sift_client.client import SiftClient class IngestionConfig(BaseType[IngestionConfigProto, "IngestionConfig"]): - """ - Model of the Sift Ingestion Config. - """ + """Model of the Sift Ingestion Config.""" asset_id: str client_key: str @@ -40,7 +39,7 @@ class IngestionConfig(BaseType[IngestionConfigProto, "IngestionConfig"]): @classmethod def _from_proto( cls, proto: IngestionConfigProto, sift_client: SiftClient | None = None - ) -> "IngestionConfig": + ) -> IngestionConfig: return cls( id_=proto.ingestion_config_id, asset_id=proto.asset_id, @@ -50,9 +49,14 @@ def _from_proto( class Flow(BaseType[FlowConfig, "Flow"]): + """Model representing a data flow for ingestion. + + A Flow represents a collection of channels that are ingested together. + """ + model_config = ConfigDict(frozen=False) name: str - channels: List[Channel] + channels: list[Channel] ingestion_config_id: str | None = None run_id: str | None = None @@ -77,11 +81,28 @@ def _to_rust_config(self) -> FlowConfigPy: ) def add_channel(self, channel: Channel): + """Add a Channel to this Flow. + + Args: + channel: The Channel to add. + + Raises: + ValueError: If the flow has already been created with an ingestion config. + """ if self.ingestion_config_id: raise ValueError("Cannot add a channel to a flow after creation") self.channels.append(channel) def ingest(self, *, timestamp: datetime, channel_values: dict[str, Any]): + """Ingest data for this Flow. + + Args: + timestamp: The timestamp of the data. + channel_values: Dictionary mapping Channel names to their values. + + Raises: + ValueError: If the ingestion config ID is not set. + """ if self.ingestion_config_id is None: raise ValueError("Ingestion config ID is not set.") self.client.ingestion.ingest( @@ -117,6 +138,7 @@ def _rust_channel_value_from_bitfield( """Helper function to convert a bitfield value to a ChannelValuePy object. Args: + channel: The channel object for the bitfield value. value: The value to convert to a ChannelValuePy object. - A single int or bytes will be treated as representing bytes directly - Dicts or list of ints will be treated as representing individual bitfield elements. @@ -209,8 +231,8 @@ def _to_rust_type(data_type: ChannelDataType) -> ChannelDataTypePy: raise ValueError(f"Unknown data type: {data_type}") -def _to_ingestion_value(type: ChannelDataType, value: Any) -> IngestWithConfigDataChannelValue: +def _to_ingestion_value(data_type: ChannelDataType, value: Any) -> IngestWithConfigDataChannelValue: if value is None: return IngestWithConfigDataChannelValue(empty=Empty()) - ingestion_type_string = type.name.lower().replace("int_", "int") + ingestion_type_string = data_type.name.lower().replace("int_", "int") return IngestWithConfigDataChannelValue(**{ingestion_type_string: value}) diff --git a/python/lib/sift_client/types/rule.py b/python/lib/sift_client/sift_types/rule.py similarity index 85% rename from python/lib/sift_client/types/rule.py rename to python/lib/sift_client/sift_types/rule.py index b11cddf79..90c40e6ab 100644 --- a/python/lib/sift_client/types/rule.py +++ b/python/lib/sift_client/sift_types/rule.py @@ -1,8 +1,8 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from enum import Enum -from typing import TYPE_CHECKING, List, Optional, Type +from typing import TYPE_CHECKING from sift.rules.v1.rules_pb2 import ( ActionKind, @@ -26,28 +26,26 @@ RuleVersion as RuleVersionProto, ) -from sift_client.types._base import BaseType, ModelUpdate -from sift_client.types.asset import Asset -from sift_client.types.channel import ChannelReference +from sift_client.sift_types._base import BaseType, ModelUpdate +from sift_client.sift_types.channel import ChannelReference if TYPE_CHECKING: from sift_client.client import SiftClient + from sift_client.sift_types.asset import Asset class Rule(BaseType[RuleProto, "Rule"]): - """ - Model of the Sift Rule. - """ + """Model of the Sift Rule.""" name: str description: str is_enabled: bool = True expression: str | None = None - channel_references: List[ChannelReference] | None = None + channel_references: list[ChannelReference] | None = None action: RuleAction | None = None - asset_ids: List[str] | None = None - asset_tag_ids: List[str] | None = None - contextual_channels: List[str] | None = None + asset_ids: list[str] | None = None + asset_tag_ids: list[str] | None = None + contextual_channels: list[str] | None = None client_key: str | None = None # Fields from proto @@ -63,10 +61,12 @@ class Rule(BaseType[RuleProto, "Rule"]): @property def is_archived(self) -> bool: """Whether the rule is archived.""" - return self.archived_date is not None and self.archived_date > datetime(1970, 1, 1) + return self.archived_date is not None and self.archived_date > datetime( + 1970, 1, 1, tzinfo=timezone.utc + ) @property - def assets(self) -> List[Asset]: + def assets(self) -> list[Asset]: """Get the assets that this rule applies to.""" return self.client.assets.list_(asset_ids=self.asset_ids, tag_ids=self.asset_tag_ids) @@ -91,11 +91,11 @@ def tags(self): raise NotImplementedError("Tags is not supported yet.") def update(self, update: RuleUpdate | dict, version_notes: str | None = None) -> Rule: - """ - Update the Rule. + """Update the Rule. Args: update: Either a RuleUpdate instance or a dictionary of key-value pairs to update. + version_notes: Notes associated with the change. """ updated_rule = self.client.rules.update( rule=self, update=update, version_notes=version_notes @@ -146,8 +146,7 @@ def _from_proto(cls, proto: RuleProto, sift_client: SiftClient | None = None) -> class RuleUpdate(ModelUpdate[RuleProto]): - """ - Model of the Rule fields that can be updated. + """Model of the Rule fields that can be updated. Note: - asset_ids applies this rule to those assets. @@ -157,13 +156,13 @@ class RuleUpdate(ModelUpdate[RuleProto]): name: str | None = None description: str | None = None expression: str | None = None - channel_references: List[ChannelReference] | None = None + channel_references: list[ChannelReference] | None = None action: RuleAction | None = None - asset_ids: List[str] | None = None - asset_tag_ids: List[str] | None = None - contextual_channels: List[str] | None = None + asset_ids: list[str] | None = None + asset_tag_ids: list[str] | None = None + contextual_channels: list[str] | None = None - def _get_proto_class(self) -> Type[RuleProto]: + def _get_proto_class(self) -> type[RuleProto]: return RuleProto def _add_resource_id_to_proto(self, proto_msg: RuleProto): @@ -180,7 +179,15 @@ class RuleActionType(Enum): WEBHOOK = ActionKind.WEBHOOK # 2 @classmethod - def from_str(cls, val: str) -> Optional["RuleActionType"]: + def from_str(cls, val: str) -> RuleActionType | None: + """Convert string representation to RuleActionType. + + Args: + val: String representation of RuleActionType. + + Returns: + RuleActionType if conversion is successful, None otherwise. + """ if isinstance(val, str) and val.startswith("ACTION_KIND_"): for item in cls: if "ACTION_KIND_" + item.name == val: @@ -197,7 +204,15 @@ class RuleAnnotationType(Enum): PHASE = 2 @classmethod - def from_str(cls, val: str) -> Optional["RuleAnnotationType"]: + def from_str(cls, val: str) -> RuleAnnotationType | None: + """Convert string representation to RuleAnnotationType. + + Args: + val: String representation of RuleAnnotationType. + + Returns: + RuleAnnotationType if conversion is successful, None otherwise. + """ if isinstance(val, str) and val.startswith("ANNOTATION_TYPE_"): for item in cls: if "ANNOTATION_TYPE_" + item.name == val: @@ -207,9 +222,7 @@ def from_str(cls, val: str) -> Optional["RuleAnnotationType"]: class RuleAction(BaseType[RuleActionProto, "RuleAction"]): - """ - Model of a Rule Action. - """ + """Model of a Rule Action.""" action_type: RuleActionType condition_id: str | None = None @@ -219,14 +232,14 @@ class RuleAction(BaseType[RuleActionProto, "RuleAction"]): modified_by_user_id: str | None = None version_id: str | None = None annotation_type: RuleAnnotationType | None = None - tags: List[str] | None = None + tags: list[str] | None = None default_assignee_user_id: str | None = None @classmethod def annotation( cls, annotation_type: RuleAnnotationType, - tags: List[str], + tags: list[str], default_assignee_user_id: str | None = None, ) -> RuleAction: """Create an annotation action. @@ -292,9 +305,7 @@ def _to_update_request(self) -> UpdateActionRequest: class RuleVersion(BaseType[RuleVersionProto, "RuleVersion"]): - """ - Model of a Rule Version. - """ + """Model of a Rule Version.""" rule_id: str rule_version_id: str diff --git a/python/lib/sift_client/types/run.py b/python/lib/sift_client/sift_types/run.py similarity index 78% rename from python/lib/sift_client/types/run.py rename to python/lib/sift_client/sift_types/run.py index bd6ca3f93..3549e75be 100644 --- a/python/lib/sift_client/types/run.py +++ b/python/lib/sift_client/sift_types/run.py @@ -1,23 +1,21 @@ from __future__ import annotations -from datetime import datetime -from typing import TYPE_CHECKING, List, Type +from datetime import datetime, timezone +from typing import TYPE_CHECKING, ClassVar from pydantic import ConfigDict from sift.runs.v2.runs_pb2 import Run as RunProto -from sift_client.types._base import BaseType, MappingHelper, ModelUpdate +from sift_client.sift_types._base import BaseType, MappingHelper, ModelUpdate from sift_client.util.metadata import metadata_dict_to_proto, metadata_proto_to_dict if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.types.asset import Asset + from sift_client.sift_types.asset import Asset class RunUpdate(ModelUpdate[RunProto]): - """ - Update model for Run. - """ + """Update model for Run.""" model_config = ConfigDict(arbitrary_types_allowed=True) @@ -27,16 +25,16 @@ class RunUpdate(ModelUpdate[RunProto]): stop_time: datetime | None = None is_pinned: bool | None = None client_key: str | None = None - tags: List[str] | None = None + tags: list[str] | None = None metadata: dict[str, str | float | bool] | None = None - _to_proto_helpers = { + _to_proto_helpers: ClassVar = { "metadata": MappingHelper( proto_attr_path="metadata", update_field="metadata", converter=metadata_dict_to_proto ), } - def _get_proto_class(self) -> Type[RunProto]: + def _get_proto_class(self) -> type[RunProto]: return RunProto def _add_resource_id_to_proto(self, proto_msg: RunProto): @@ -46,9 +44,7 @@ def _add_resource_id_to_proto(self, proto_msg: RunProto): class Run(BaseType[RunProto, "Run"]): - """ - Run model representing a data collection run. - """ + """Run model representing a data collection run.""" model_config = ConfigDict(arbitrary_types_allowed=True) @@ -61,24 +57,28 @@ class Run(BaseType[RunProto, "Run"]): organization_id: str start_time: datetime | None = None stop_time: datetime | None = None - tags: List[str] | None = None + tags: list[str] | None = None default_report_id: str | None = None client_key: str | None = None metadata: dict[str, str | float | bool] - asset_ids: List[str] | None = None + asset_ids: list[str] | None = None archived_date: datetime | None = None @classmethod def _from_proto(cls, proto: RunProto, sift_client: SiftClient | None = None) -> Run: return cls( id_=proto.run_id, - created_date=proto.created_date.ToDatetime(), - modified_date=proto.modified_date.ToDatetime(), + created_date=proto.created_date.ToDatetime(tzinfo=timezone.utc), + modified_date=proto.modified_date.ToDatetime(tzinfo=timezone.utc), created_by_user_id=proto.created_by_user_id, modified_by_user_id=proto.modified_by_user_id, organization_id=proto.organization_id, - start_time=proto.start_time.ToDatetime() if proto.HasField("start_time") else None, - stop_time=proto.stop_time.ToDatetime() if proto.HasField("stop_time") else None, + start_time=proto.start_time.ToDatetime(tzinfo=timezone.utc) + if proto.HasField("start_time") + else None, + stop_time=proto.stop_time.ToDatetime(tzinfo=timezone.utc) + if proto.HasField("stop_time") + else None, name=proto.name, description=proto.description, tags=list(proto.tags), @@ -93,9 +93,7 @@ def _from_proto(cls, proto: RunProto, sift_client: SiftClient | None = None) -> ) def _to_proto(self) -> RunProto: - """ - Convert to protobuf message. - """ + """Convert to protobuf message.""" proto = RunProto( run_id=self.id_ or "", created_date=self.created_date, # type: ignore @@ -129,10 +127,8 @@ def _to_proto(self) -> RunProto: return proto @property - def assets(self) -> List[Asset]: - """ - Return all assets associated with this run. - """ + def assets(self) -> list[Asset]: + """Return all assets associated with this run.""" if not hasattr(self, "client") or self.client is None: raise RuntimeError("Run is not bound to a client instance.") if not self.asset_ids: diff --git a/python/lib/sift_client/transport/__init__.py b/python/lib/sift_client/transport/__init__.py index 299e85fc8..249d9bc7e 100644 --- a/python/lib/sift_client/transport/__init__.py +++ b/python/lib/sift_client/transport/__init__.py @@ -7,11 +7,11 @@ from sift_client.transport.rest_transport import RestClient, RestConfig __all__ = [ - "SiftConnectionConfig", - "WithGrpcClient", - "WithRestClient", "GrpcClient", "GrpcConfig", "RestClient", "RestConfig", + "SiftConnectionConfig", + "WithGrpcClient", + "WithRestClient", ] diff --git a/python/lib/sift_client/transport/base_connection.py b/python/lib/sift_client/transport/base_connection.py index 8b914827c..f98b0282a 100644 --- a/python/lib/sift_client/transport/base_connection.py +++ b/python/lib/sift_client/transport/base_connection.py @@ -1,15 +1,20 @@ from __future__ import annotations -import asyncio from abc import ABC +from typing import TYPE_CHECKING from sift_client.transport.grpc_transport import GrpcClient, GrpcConfig from sift_client.transport.rest_transport import RestClient, RestConfig +if TYPE_CHECKING: + import asyncio + class SiftConnectionConfig: - """ - Configuration for Grpc and Rest cnnections. + """Configuration for Grpc and Rest connections. + + This class provides a unified configuration for both gRPC and REST connections, + allowing for consistent settings across different transport protocols. """ def __init__( @@ -20,6 +25,15 @@ def __init__( use_ssl: bool = False, cert_via_openssl: bool = False, ): + """Initialize the connection configuration. + + Args: + grpc_url: The URL for the gRPC service. + rest_url: The URL for the REST service. + api_key: The API key for authentication. + use_ssl: Whether to use SSL/TLS for secure connections. + cert_via_openssl: Whether to use OpenSSL for certificate validation. + """ self.api_key = api_key self.grpc_url = grpc_url self.rest_url = rest_url @@ -27,6 +41,11 @@ def __init__( self.cert_via_openssl = cert_via_openssl def get_grpc_config(self): + """Create and return a GrpcConfig with the current settings. + + Returns: + A GrpcConfig object configured with this instance's settings. + """ return GrpcConfig( url=self.grpc_url, api_key=self.api_key, @@ -35,6 +54,11 @@ def get_grpc_config(self): ) def get_rest_config(self): + """Create and return a RestConfig with the current settings. + + Returns: + A RestConfig object configured with this instance's settings. + """ return RestConfig( base_url=self.rest_url, api_key=self.api_key, @@ -44,24 +68,42 @@ def get_rest_config(self): class WithGrpcClient(ABC): + """Abstract base class for classes that require a gRPC client. + + This class provides access to a gRPC client for making API calls. + """ + _grpc_client: GrpcClient def __init__(self, grpc_client: GrpcClient): + """Initialize with a gRPC client. + + Args: + grpc_client: The gRPC client to use for API calls. + """ self._grpc_client = grpc_client def get_asyncio_loop(self) -> asyncio.AbstractEventLoop: - """ - Gets the default asyncio loop used by the gRPC client. + """Gets the default asyncio loop used by the gRPC client. Returns: The default asyncio loop. - """ return self._grpc_client.default_loop class WithRestClient(ABC): + """Abstract base class for classes that require a REST client. + + This class provides access to a REST client for making API calls. + """ + _rest_client: RestClient def __init__(self, rest_client: RestClient): + """Initialize with a REST client. + + Args: + rest_client: The REST client to use for API calls. + """ self._rest_client = rest_client diff --git a/python/lib/sift_client/transport/grpc_transport.py b/python/lib/sift_client/transport/grpc_transport.py index e2b3a3832..b27ce8fc1 100644 --- a/python/lib/sift_client/transport/grpc_transport.py +++ b/python/lib/sift_client/transport/grpc_transport.py @@ -1,5 +1,4 @@ -""" -Transport layer for gRPC communication. +"""Transport layer for gRPC communication. This module provides a simple wrapper around sift_py/grpc/transport.py for making gRPC API calls. It just stores the channel and the stubs, without any additional functionality. @@ -11,7 +10,7 @@ import atexit import logging import threading -from typing import Any, Dict, Optional, Type +from typing import Any from sift_py.grpc.transport import ( SiftChannelConfig, @@ -23,8 +22,7 @@ def _suppress_blocking_io(loop, context): - """ - Suppress benign BlockingIOError from gRPC's PollerCompletionQueue. + """Suppress benign BlockingIOError from gRPC's PollerCompletionQueue. gRPC's internal poller uses non-blocking I/O. When no events are ready, it raises BlockingIOError (EAGAIN), which is expected and safe to ignore. @@ -45,10 +43,9 @@ def __init__( api_key: str, use_ssl: bool = True, cert_via_openssl: bool = False, - metadata: Dict[str, str] | None = None, + metadata: dict[str, str] | None = None, ): - """ - Initialize the gRPC configuration. + """Initialize the gRPC configuration. Args: url: The URI of the gRPC server. @@ -65,8 +62,7 @@ def __init__( self.metadata = metadata or {} def _to_sift_channel_config(self) -> SiftChannelConfig: - """ - Convert to a SiftChannelConfig. + """Convert to a SiftChannelConfig. Returns: A SiftChannelConfig. @@ -80,23 +76,21 @@ def _to_sift_channel_config(self) -> SiftChannelConfig: class GrpcClient: - """ - A simple wrapper around sift_py/grpc/transport.py for making gRPC API calls. + """A simple wrapper around sift_py/grpc/transport.py for making gRPC API calls. This class just stores the channel and the stubs, without any additional functionality. """ def __init__(self, config: GrpcConfig): - """ - Initialize the gRPC client. + """Initialize the gRPC client. Args: config: The gRPC client configuration. """ self._config = config # map each asyncio loop to its async channel and stub dict - self._channels_async: Dict[asyncio.AbstractEventLoop, Any] = {} - self._stubs_async_map: Dict[asyncio.AbstractEventLoop, Dict[Type[Any], Any]] = {} + self._channels_async: dict[asyncio.AbstractEventLoop, Any] = {} + self._stubs_async_map: dict[asyncio.AbstractEventLoop, dict[type[Any], Any]] = {} # default loop for sync API self._default_loop = asyncio.new_event_loop() atexit.register(self.close_sync) @@ -124,11 +118,15 @@ def _run_default_loop(): @property def default_loop(self) -> asyncio.AbstractEventLoop: - return self._default_loop + """Return the default event loop used for synchronous API operations. - def get_stub(self, stub_class: Type[Any]) -> Any: + Returns: + The default asyncio event loop. """ - Get an async stub bound to the current event loop. + return self._default_loop + + def get_stub(self, stub_class: type[Any]) -> Any: + """Get an async stub bound to the current event loop. Creates a channel and stub for this loop if needed. """ try: @@ -180,7 +178,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.close_sync() async def _create_async_channel( - self, cfg: SiftChannelConfig, metadata: Optional[Dict[str, str]] + self, cfg: SiftChannelConfig, metadata: dict[str, str] | None ) -> Any: """Helper to create async channel on default loop.""" return use_sift_async_channel(cfg, metadata) diff --git a/python/lib/sift_client/transport/rest_transport.py b/python/lib/sift_client/transport/rest_transport.py index 8f01cc667..071515c94 100644 --- a/python/lib/sift_client/transport/rest_transport.py +++ b/python/lib/sift_client/transport/rest_transport.py @@ -1,5 +1,4 @@ -""" -Transport layer for REST communication. +"""Transport layer for REST communication. This module provides a simple wrapper around sift_py/rest.py for making REST API calls. """ @@ -7,11 +6,15 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING from urllib.parse import urljoin -import requests from sift_py.rest import _DEFAULT_REST_RETRY, SiftRestConfig, _RestService -from urllib3.util import Retry + +if TYPE_CHECKING: + import requests + from urllib3.util import Retry + # Configure logging logger = logging.getLogger(__name__) @@ -28,14 +31,14 @@ def __init__( cert_via_openssl: bool = False, retry: Retry = _DEFAULT_REST_RETRY, ): - """ - Initialize the REST configuration. + """Initialize the REST configuration. Args: base_url: The base URL of the API. api_key: The API key for authentication. use_ssl: Whether to use HTTPS. cert_via_openssl: Whether to use OpenSSL for SSL/TLS. + retry: The retry configuration for requests. """ self.base_url = base_url self.api_key = api_key @@ -44,13 +47,11 @@ def __init__( self.retry = retry def _to_sift_rest_config(self) -> SiftRestConfig: - """ - Convert to a SiftRestConfig for backwards compatibility. Will be removed in the future. + """Convert to a SiftRestConfig for backwards compatibility. Will be removed in the future. Returns: A SiftRestConfig. """ - return { "uri": self.base_url, "apikey": self.api_key, @@ -61,16 +62,14 @@ def _to_sift_rest_config(self) -> SiftRestConfig: class RestClient: - """ - A client wrapper for REST APIs. + """A client wrapper for REST APIs. This class provides a wrapper around sift_py/rest.py for making REST API calls. It handles authentication, retries, and error mapping. """ def __init__(self, config: RestConfig): - """ - Initialize the REST client. + """Initialize the REST client. Args: config: The REST client configuration. @@ -80,8 +79,7 @@ def __init__(self, config: RestConfig): self._client = self._create_client() def _create_client(self) -> _RestService: - """ - Create a REST service with the configured settings. Using _RestService for backwards compatibility. Will be removed in the future. + """Create a REST service with the configured settings. Using _RestService for backwards compatibility. Will be removed in the future. Returns: A configured REST service. @@ -97,9 +95,15 @@ def __init__(self, rest_conf: SiftRestConfig): @property def base_url(self) -> str: + """Get the base URL of the REST client. + + Returns: + The base URL string. + """ return self._base_url def close(self) -> None: + """Close the REST client session.""" self._client._session.close() # Convenience methods for common HTTP methods @@ -115,22 +119,75 @@ def _execute( return self._client._session.request(method, full_url, headers=headers, data=data, **kwargs) def get(self, endpoint: str, headers: dict | None = None, **kwargs) -> requests.Response: + """Execute a GET request. + + Args: + endpoint: The API endpoint to call. + headers: Additional headers to include in the request. + **kwargs: Additional arguments to pass to the request. + + Returns: + The HTTP response. + """ return self._execute("GET", endpoint=endpoint, headers=headers, **kwargs) def post( self, endpoint: str, headers: dict | None = None, data=None, **kwargs ) -> requests.Response: + """Execute a POST request. + + Args: + endpoint: The API endpoint to call. + headers: Additional headers to include in the request. + data: The data to send in the request body. + **kwargs: Additional arguments to pass to the request. + + Returns: + The HTTP response. + """ return self._execute("POST", endpoint=endpoint, headers=headers, data=data, **kwargs) def put( self, endpoint: str, headers: dict | None = None, data=None, **kwargs ) -> requests.Response: + """Execute a PUT request. + + Args: + endpoint: The API endpoint to call. + headers: Additional headers to include in the request. + data: The data to send in the request body. + **kwargs: Additional arguments to pass to the request. + + Returns: + The HTTP response. + """ return self._execute("PUT", endpoint=endpoint, headers=headers, data=data, **kwargs) def delete(self, endpoint: str, headers: dict | None = None, **kwargs) -> requests.Response: + """Execute a DELETE request. + + Args: + endpoint: The API endpoint to call. + headers: Additional headers to include in the request. + **kwargs: Additional arguments to pass to the request. + + Returns: + The HTTP response. + """ return self._execute("DELETE", endpoint=endpoint, headers=headers, **kwargs) def patch( self, endpoint: str, headers: dict | None = None, data=None, **kwargs ) -> requests.Response: + """Execute a PATCH request. + + Args: + endpoint: The API endpoint to call. + headers: Additional headers to include in the request. + data: The data to send in the request body. + **kwargs: Additional arguments to pass to the request. + + Returns: + The HTTP response. + """ return self._execute("PATCH", endpoint=endpoint, headers=headers, data=data, **kwargs) diff --git a/python/lib/sift_client/util/__init__.py b/python/lib/sift_client/util/__init__.py index dbe0a253c..a94158f48 100644 --- a/python/lib/sift_client/util/__init__.py +++ b/python/lib/sift_client/util/__init__.py @@ -1,3 +1 @@ -""" -Utility modules for the sift_client package. -""" +"""Utility modules for the sift_client package.""" diff --git a/python/lib/sift_client/util/cel_utils.py b/python/lib/sift_client/util/cel_utils.py index d6e0a38b5..219f6fe19 100644 --- a/python/lib/sift_client/util/cel_utils.py +++ b/python/lib/sift_client/util/cel_utils.py @@ -1,5 +1,4 @@ -""" -CEL (Common Expression Language) utilities for generating CEL expressions. +"""CEL (Common Expression Language) utilities for generating CEL expressions. This module provides helper functions to generate CEL expressions for building filters commonly used in Sift. """ @@ -12,8 +11,7 @@ def in_(field: str, vals: list[str]) -> str: - """ - Generates a CEL expression that checks for `field` membership in `vals`. + """Generates a CEL expression that checks for `field` membership in `vals`. Args: field: The field name to check @@ -30,8 +28,7 @@ def in_(field: str, vals: list[str]) -> str: def parens(expr: str) -> str: - """ - Wraps the given expression in parentheses. + """Wraps the given expression in parentheses. Args: expr: The expression to wrap in parentheses @@ -43,8 +40,7 @@ def parens(expr: str) -> str: def equals(key: str, value: Any) -> str: - """ - Generates a CEL expression that checks for equality. + """Generates a CEL expression that checks for equality. Args: key: The field name @@ -62,8 +58,7 @@ def equals(key: str, value: Any) -> str: def equals_all(values: dict[str, Any]) -> str: - """ - Generates a CEL expression that checks for equality of all key-value pairs. + """Generates a CEL expression that checks for equality of all key-value pairs. Args: values: Dictionary of field names and values to check for equality @@ -76,8 +71,7 @@ def equals_all(values: dict[str, Any]) -> str: def equals_any(values: dict[str, Any]) -> str: - """ - Generates a CEL expression that checks for equality of any key-value pairs. + """Generates a CEL expression that checks for equality of any key-value pairs. Args: values: Dictionary of field names and values to check for equality @@ -90,8 +84,7 @@ def equals_any(values: dict[str, Any]) -> str: def equals_double(key: str, value: Any) -> str: - """ - Generates a CEL expression that checks for equality with a double value. + """Generates a CEL expression that checks for equality with a double value. Args: key: The field name @@ -106,8 +99,7 @@ def equals_double(key: str, value: Any) -> str: def equals_null(key: str) -> str: - """ - Generates a CEL expression that checks for equality with null. + """Generates a CEL expression that checks for equality with null. Args: key: The field name @@ -119,8 +111,7 @@ def equals_null(key: str) -> str: def and_(*clauses: str) -> str: - """ - Generates a CEL expression that joins all clauses with an AND operator. + """Generates a CEL expression that joins all clauses with an AND operator. Args: *clauses: Variable number of CEL expression strings @@ -136,8 +127,7 @@ def and_(*clauses: str) -> str: def or_(*clauses: str) -> str: - """ - Generates a CEL expression that joins all clauses with an OR operator. + """Generates a CEL expression that joins all clauses with an OR operator. Args: *clauses: Variable number of CEL expression strings @@ -153,8 +143,7 @@ def or_(*clauses: str) -> str: def not_(clause: str) -> str: - """ - Generates a CEL expression that negates the given clause. + """Generates a CEL expression that negates the given clause. Args: clause: The CEL expression to negate @@ -166,8 +155,7 @@ def not_(clause: str) -> str: def contains(field: str, value: str) -> str: - """ - Generates a CEL expression that checks whether a string field contains a given value. + """Generates a CEL expression that checks whether a string field contains a given value. Args: field: The field name @@ -180,8 +168,7 @@ def contains(field: str, value: str) -> str: def match(field: str, query: str | re.Pattern) -> str: - """ - Generates a CEL expression that checks for a match on the specified field. + """Generates a CEL expression that checks for a match on the specified field. Args: field: The field name @@ -198,8 +185,7 @@ def match(field: str, query: str | re.Pattern) -> str: def greater_than(field: str, value: int | float | datetime) -> str: - """ - Generates a CEL expression that checks whether a numeric or datetime field is greater than a given value. + """Generates a CEL expression that checks whether a numeric or datetime field is greater than a given value. Args: field: The field name @@ -216,8 +202,7 @@ def greater_than(field: str, value: int | float | datetime) -> str: def less_than(field: str, value: int | float | datetime) -> str: - """ - Generates a CEL expression that checks whether a numeric or datetime field is less than a given value. + """Generates a CEL expression that checks whether a numeric or datetime field is less than a given value. Args: field: The field name diff --git a/python/lib/sift_client/util/metadata.py b/python/lib/sift_client/util/metadata.py index d604ca162..0b31fecce 100644 --- a/python/lib/sift_client/util/metadata.py +++ b/python/lib/sift_client/util/metadata.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from sift.metadata.v1.metadata_pb2 import ( MetadataKey, MetadataKeyType, @@ -10,13 +8,9 @@ MetadataValue as MetadataProto, ) -if TYPE_CHECKING: - pass - def metadata_dict_to_proto(_metadata: dict[str, str | float | bool]) -> list[MetadataProto]: - """ - Converts metadata dictionary into a list of MetadataValue objects. + """Converts metadata dictionary into a list of MetadataValue objects. Args: _metadata: Dictionary of metadata key-value pairs. @@ -27,25 +21,25 @@ def metadata_dict_to_proto(_metadata: dict[str, str | float | bool]) -> list[Met metadata = [] for key, value in _metadata.items(): - type = MetadataKeyType.METADATA_KEY_TYPE_UNSPECIFIED + metadata_key_type = MetadataKeyType.METADATA_KEY_TYPE_UNSPECIFIED string_value = None boolean_value = None number_value = None if isinstance(value, str): string_value = value - type = MetadataKeyType.METADATA_KEY_TYPE_STRING + metadata_key_type = MetadataKeyType.METADATA_KEY_TYPE_STRING elif isinstance(value, bool): # Need to check bool before int since python thinks "True" is an int boolean_value = value - type = MetadataKeyType.METADATA_KEY_TYPE_BOOLEAN + metadata_key_type = MetadataKeyType.METADATA_KEY_TYPE_BOOLEAN elif isinstance(value, (int, float)): number_value = value - type = MetadataKeyType.METADATA_KEY_TYPE_NUMBER + metadata_key_type = MetadataKeyType.METADATA_KEY_TYPE_NUMBER else: raise ValueError(f"Unsupported metadata value type for key '{key}': {value}") - wrapped_key = MetadataKey(name=key, type=type) + wrapped_key = MetadataKey(name=key, type=metadata_key_type) wrapped_value = MetadataProto( key=wrapped_key, string_value=string_value, # type: ignore @@ -58,8 +52,7 @@ def metadata_dict_to_proto(_metadata: dict[str, str | float | bool]) -> list[Met def metadata_proto_to_dict(metadata: list[MetadataProto]) -> dict[str, str | float | bool]: - """ - Converts a list of MetadataValue objects into a dictionary. + """Converts a list of MetadataValue objects into a dictionary. Args: metadata: List of MetadataValue objects. @@ -67,7 +60,6 @@ def metadata_proto_to_dict(metadata: list[MetadataProto]) -> dict[str, str | flo Returns: Dictionary of metadata key-value pairs. """ - unwrapped_metadata: dict[str, str | float | bool] = {} for md in metadata: if md.key.name in unwrapped_metadata: diff --git a/python/lib/sift_client/util/timestamp.py b/python/lib/sift_client/util/timestamp.py index 96844560c..0d33689f2 100644 --- a/python/lib/sift_client/util/timestamp.py +++ b/python/lib/sift_client/util/timestamp.py @@ -5,12 +5,28 @@ def to_pb_timestamp(timestamp: datetime) -> Timestamp: + """Convert a Python datetime to a Protocol Buffer Timestamp. + + Args: + timestamp: The datetime to convert + + Returns: + A Protocol Buffer Timestamp representation + """ timestamp_pb = Timestamp() timestamp_pb.FromDatetime(timestamp) return timestamp_pb def to_rust_py_timestamp(time: datetime) -> TimeValuePy: + """Convert a Python datetime to a Rust TimeValuePy. + + Args: + time: The datetime to convert + + Returns: + A TimeValuePy representation + """ ts = time.timestamp() secs = int(ts) nsecs = int((ts - secs) * 1_000_000_000) diff --git a/python/lib/sift_client/util/util.py b/python/lib/sift_client/util/util.py index 1c83ccc58..4202ee715 100644 --- a/python/lib/sift_client/util/util.py +++ b/python/lib/sift_client/util/util.py @@ -1,16 +1,17 @@ from __future__ import annotations -from typing import NamedTuple - -from sift_client.resources import ( - AssetsAPIAsync, - CalculatedChannelsAPIAsync, - ChannelsAPIAsync, - IngestionAPIAsync, - PingAPIAsync, - RulesAPIAsync, - RunsAPIAsync, -) +from typing import TYPE_CHECKING, NamedTuple + +if TYPE_CHECKING: + from sift_client.resources import ( + AssetsAPIAsync, + CalculatedChannelsAPIAsync, + ChannelsAPIAsync, + IngestionAPIAsync, + PingAPIAsync, + RulesAPIAsync, + RunsAPIAsync, + ) class AsyncAPIs(NamedTuple): diff --git a/python/pyproject.toml b/python/pyproject.toml index 4a5726467..a5bf57bfe 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -44,6 +44,7 @@ Repository = "https://github.com/sift-stack/sift/tree/main/python" Changelog = "https://github.com/sift-stack/sift/tree/main/python/CHANGELOG.md" [project.optional-dependencies] +# development development = [ "grpcio-testing~=1.13", "mypy==1.10.0", @@ -57,6 +58,8 @@ development = [ ] build = ["pdoc==14.5.0", "build==1.2.1"] docs = ["mkdocs", "mkdocs-material", "mkdocstrings[python]", "mkdocs-include-markdown-plugin", "mkdocs-api-autonav", "mike"] + +# May be required for certain library functionality openssl = ["pyOpenSSL<24.0.0", "types-pyOpenSSL<24.0.0", "cffi~=1.14"] tdms = ["npTDMS~=1.9"] rosbags = ["rosbags~=0.0"] @@ -182,4 +185,10 @@ exclude = [ ] [tool.ruff.lint] -select = ["F", "W", "I", "N", "TID"] +select = [ + "F", # pyflakes + "W", # pycodestyle warning + "I", # import sort + "N", # pep8-naming + "TID", # flake8-tidy-imports +] diff --git a/python/scripts/dev b/python/scripts/dev index 3482fa13c..8565c84e9 100755 --- a/python/scripts/dev +++ b/python/scripts/dev @@ -31,8 +31,7 @@ EOT pip_install() { source venv/bin/activate - pip install '.[development]' - pip install '.[build]' + pip install '.[development,build,docs,openssl,tdms,rosbags,sift-stream,hdf5]' pip install -e . } @@ -99,7 +98,7 @@ gen_stubs() { python3 sift_client/_internal/gen_pyi.py sift_client/resources/sync_stubs cd .. ruff format ./lib/sift_client/resources/sync_stubs/*.pyi -q - ruff check ./lib/sift_client/resources/sync_stubs/*.pyi --fix -q + ruff check ./lib/sift_client/resources/sync_stubs/*.pyi --fix --unsafe-fixes -q } mypy_stubs() {