From 6658b4bcc0344b48facd2807227aa5d2c7a60de9 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 09:57:12 -0800 Subject: [PATCH 001/127] Added remote files to python client --- .../_internal/low_level_wrappers/__init__.py | 2 + .../low_level_wrappers/remote_files.py | 162 ++++++++++++++++++ python/lib/sift_client/client.py | 7 + python/lib/sift_client/resources/__init__.py | 4 + .../lib/sift_client/resources/remote_files.py | 148 ++++++++++++++++ .../resources/sync_stubs/__init__.py | 16 +- .../resources/sync_stubs/__init__.pyi | 105 ++++++++++++ .../lib/sift_client/sift_types/remote_file.py | 129 ++++++++++++++ python/lib/sift_client/util/util.py | 4 + 9 files changed, 575 insertions(+), 2 deletions(-) create mode 100644 python/lib/sift_client/_internal/low_level_wrappers/remote_files.py create mode 100644 python/lib/sift_client/resources/remote_files.py create mode 100644 python/lib/sift_client/sift_types/remote_file.py diff --git a/python/lib/sift_client/_internal/low_level_wrappers/__init__.py b/python/lib/sift_client/_internal/low_level_wrappers/__init__.py index d4122d3aa..39caca801 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/__init__.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/__init__.py @@ -5,6 +5,7 @@ from sift_client._internal.low_level_wrappers.channels import ChannelsLowLevelClient from sift_client._internal.low_level_wrappers.ingestion import IngestionLowLevelClient from sift_client._internal.low_level_wrappers.ping import PingLowLevelClient +from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient from sift_client._internal.low_level_wrappers.reports import ReportsLowLevelClient from sift_client._internal.low_level_wrappers.rules import RulesLowLevelClient from sift_client._internal.low_level_wrappers.runs import RunsLowLevelClient @@ -18,6 +19,7 @@ "ChannelsLowLevelClient", "IngestionLowLevelClient", "PingLowLevelClient", + "RemoteFilesLowLevelClient", "ReportsLowLevelClient", "RulesLowLevelClient", "RunsLowLevelClient", diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py new file mode 100644 index 000000000..0ef62a146 --- /dev/null +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +from typing import Any, cast + +from sift.remote_files.v1.remote_files_pb2 import ( + BatchDeleteRemoteFilesRequest, + BatchDeleteRemoteFilesResponse, + CreateRemoteFileRequest, + CreateRemoteFileResponse, + DeleteRemoteFileRequest, + DeleteRemoteFileResponse, + GetRemoteFileDownloadUrlRequest, + GetRemoteFileDownloadUrlResponse, + GetRemoteFileRequest, + GetRemoteFileResponse, + ListRemoteFilesRequest, + ListRemoteFilesResponse, + UpdateRemoteFileRequest, + UpdateRemoteFileResponse, +) +from sift.remote_files.v1.remote_files_pb2_grpc import RemoteFileServiceStub + +from sift_client._internal.low_level_wrappers.base import ( + LowLevelClientBase, +) +from sift_client.sift_types.remote_file import RemoteFile, RemoteFileUpdate +from sift_client.transport import GrpcClient, WithGrpcClient + + +class RemoteFilesLowLevelClient(LowLevelClientBase, WithGrpcClient): + """Low-level client for the RemoteFilesAPI. + + This class provides a thin wrapper around the autogenerated bindings for the RemoteFilesAPI. + """ + + def __init__(self, grpc_client: GrpcClient): + """Initialize the RemoteFilesLowLevelClient. + + Args: + grpc_client: The gRPC client to use for making API calls. + """ + super().__init__(grpc_client) + + async def get_remote_file(self, remote_file_id: str) -> RemoteFile: + """Get a remote file by ID. + + Args: + remote_file_id: The ID of the remote file to retrieve. + + Returns: + The RemoteFile. + """ + request = GetRemoteFileRequest(remote_file_id=remote_file_id) + response = await self._grpc_client.get_stub(RemoteFileServiceStub).GetRemoteFile(request) + grpc_remote_file = cast("GetRemoteFileResponse", response).remote_file + return RemoteFile._from_proto(grpc_remote_file) + + async def list_all_remote_files( + self, + query_filter: str | None = None, + order_by: str | None = None, + max_results: int | None = None, + page_size: int | None = None, + ) -> list[RemoteFile]: + """List all remote files matching the given query. + + Args: + query_filter: The CEL query filter. + order_by: The field to order by. + max_results: The maximum number of results to return. + page_size: The number of results to return per page. + + Returns: + A list of RemoteFiles matching the given query. + """ + return await self._handle_pagination( + self.list_remote_files, + kwargs={"query_filter": query_filter}, + page_size=page_size, + order_by=order_by, + max_results=max_results, + ) + + async def list_remote_files( + self, + page_size: int | None = None, + page_token: str | None = None, + query_filter: str | None = None, + order_by: str | None = None, + ) -> tuple[list[RemoteFile], str]: + """List remote files with pagination support. + + Args: + page_size: The number of results to return per page. + page_token: The page token for pagination. + query_filter: The CEL query filter. + order_by: The field to order by. + + Returns: + A tuple of (list of RemoteFiles, next_page_token). + """ + request_kwargs: dict[str, Any] = {} + if page_size is not None: + request_kwargs["page_size"] = page_size + if page_token is not None: + request_kwargs["page_token"] = page_token + if query_filter is not None: + request_kwargs["filter"] = query_filter + if order_by is not None: + request_kwargs["order_by"] = order_by + + request = ListRemoteFilesRequest(**request_kwargs) + response = await self._grpc_client.get_stub(RemoteFileServiceStub).ListRemoteFiles(request) + response = cast("ListRemoteFilesResponse", response) + return [RemoteFile._from_proto(rf) for rf in response.remote_files], response.next_page_token + + async def update_remote_file(self, update: RemoteFileUpdate) -> RemoteFile: + """Update a remote file. + + Args: + update: The RemoteFileUpdate containing the fields to update. + + Returns: + The updated RemoteFile. + """ + grpc_remote_file, update_mask = update.to_proto_with_mask() + request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask) + response = await self._grpc_client.get_stub(RemoteFileServiceStub).UpdateRemoteFile(request) + updated_grpc_remote_file = cast("UpdateRemoteFileResponse", response).remote_file + return RemoteFile._from_proto(updated_grpc_remote_file) + + async def delete_remote_file(self, remote_file_id: str) -> None: + """Delete a remote file. + + Args: + remote_file_id: The ID of the remote file to delete. + """ + request = DeleteRemoteFileRequest(remote_file_id=remote_file_id) + await self._grpc_client.get_stub(RemoteFileServiceStub).DeleteRemoteFile(request) + + async def batch_delete_remote_files(self, remote_file_ids: list[str]) -> None: + """Batch delete remote files. + + Args: + remote_file_ids: The IDs of the remote files to delete (up to 1000). + """ + request = BatchDeleteRemoteFilesRequest(remote_file_ids=remote_file_ids) + await self._grpc_client.get_stub(RemoteFileServiceStub).BatchDeleteRemoteFiles(request) + + async def get_remote_file_download_url(self, remote_file_id: str) -> str: + """Get a download URL for a remote file. + + Args: + remote_file_id: The ID of the remote file. + + Returns: + The download URL for the remote file. + """ + request = GetRemoteFileDownloadUrlRequest(remote_file_id=remote_file_id) + response = await self._grpc_client.get_stub(RemoteFileServiceStub).GetRemoteFileDownloadUrl(request) + return cast("GetRemoteFileDownloadUrlResponse", response).download_url + diff --git a/python/lib/sift_client/client.py b/python/lib/sift_client/client.py index 2a2252ef8..103c09738 100644 --- a/python/lib/sift_client/client.py +++ b/python/lib/sift_client/client.py @@ -11,6 +11,8 @@ IngestionAPIAsync, PingAPI, PingAPIAsync, + RemoteFilesAPI, + RemoteFilesAPIAsync, ReportsAPI, ReportsAPIAsync, RulesAPI, @@ -86,6 +88,9 @@ class SiftClient( ingestion: IngestionAPIAsync """Instance of the Ingestion API for making synchronous requests.""" + remote_files: RemoteFilesAPI + """Instance of the Remote Files API for making synchronous requests.""" + reports: ReportsAPI """Instance of the Reports API for making synchronous requests.""" @@ -141,6 +146,7 @@ def __init__( self.assets = AssetsAPI(self) self.calculated_channels = CalculatedChannelsAPI(self) self.channels = ChannelsAPI(self) + self.remote_files = RemoteFilesAPI(self) self.rules = RulesAPI(self) self.reports = ReportsAPI(self) self.runs = RunsAPI(self) @@ -153,6 +159,7 @@ def __init__( calculated_channels=CalculatedChannelsAPIAsync(self), channels=ChannelsAPIAsync(self), ingestion=IngestionAPIAsync(self), + remote_files=RemoteFilesAPIAsync(self), reports=ReportsAPIAsync(self), rules=RulesAPIAsync(self), runs=RunsAPIAsync(self), diff --git a/python/lib/sift_client/resources/__init__.py b/python/lib/sift_client/resources/__init__.py index 968fabdb3..57b158037 100644 --- a/python/lib/sift_client/resources/__init__.py +++ b/python/lib/sift_client/resources/__init__.py @@ -155,6 +155,7 @@ async def main(): from sift_client.resources.channels import ChannelsAPIAsync from sift_client.resources.ingestion import IngestionAPIAsync from sift_client.resources.ping import PingAPIAsync +from sift_client.resources.remote_files import RemoteFilesAPIAsync from sift_client.resources.reports import ReportsAPIAsync from sift_client.resources.rules import RulesAPIAsync from sift_client.resources.runs import RunsAPIAsync @@ -167,6 +168,7 @@ async def main(): CalculatedChannelsAPI, ChannelsAPI, PingAPI, + RemoteFilesAPI, ReportsAPI, RulesAPI, RunsAPI, @@ -184,6 +186,8 @@ async def main(): "IngestionAPIAsync", "PingAPI", "PingAPIAsync", + "RemoteFilesAPI", + "RemoteFilesAPIAsync", "ReportsAPI", "ReportsAPIAsync", "RulesAPI", diff --git a/python/lib/sift_client/resources/remote_files.py b/python/lib/sift_client/resources/remote_files.py new file mode 100644 index 000000000..752ca57bc --- /dev/null +++ b/python/lib/sift_client/resources/remote_files.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient +from sift_client.resources._base import ResourceBase +from sift_client.sift_types.remote_file import RemoteFileUpdate +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.remote_file import RemoteFile, RemoteFileEntityType + + +class RemoteFilesAPIAsync(ResourceBase): + """High-level API for interacting with remote files. + + This class provides a Pythonic, notebook-friendly interface for interacting with the RemoteFilesAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + + All methods in this class use the RemoteFile class from the low-level wrapper, which is a user-friendly + representation of a remote file using standard Python data structures and types. + """ + + def __init__(self, sift_client: SiftClient): + """ + Initialize the RemoteFilesAPI. + + Args: + sift_client: The Sift client to use. + """ + super().__init__(sift_client) + self._low_level_client = RemoteFilesLowLevelClient(grpc_client=self.client.grpc_client) + + async def get( + self, + *, + remote_file_id: str, + ) -> RemoteFile: + """Get a RemoteFile. + + Args: + remote_file_id: The ID of the remote file. + + Returns: + The RemoteFile. + """ + remote_file = await self._low_level_client.get_remote_file(remote_file_id=remote_file_id) + return self._apply_client_to_instance(remote_file) + + async def list_( + self, + *, + remote_file_id: str | None = None, + remote_file_ids: list[str] | None = None, + entity_id: str | None = None, + entity_ids: list[str] | None = None, + entity_type: RemoteFileEntityType | None = None, + entity_types: list[RemoteFileEntityType] | None = None, + name: str | None = None, + names: list[str] | None = None, + name_contains: str | None = None, + name_regex: str | re.Pattern | None = None, + ) -> list[RemoteFile]: + """List RemoteFiles. + + Args: + remote_file_id: The ID of the remote file. + remote_file_ids: List of remote file IDs. + entity_id: The entity ID. + entity_ids: List of entity IDs. + entity_type: The entity type. + entity_types: List of entity types. + name: The name of the file. + names: List of file names. + name_contains: String that the name should contain. + name_regex: Regex pattern for the name. + + Returns: + A list of RemoteFiles matching the filters. + """ + filter_parts = [ + *self._build_name_cel_filters( + name=name, names=names, name_contains=name_contains, name_regex=name_regex + ), + ] + if remote_file_id: + filter_parts.append(cel.equals("remote_file_id", remote_file_id)) + if remote_file_ids: + filter_parts.append(cel.in_("remote_file_id", remote_file_ids)) + if entity_id: + filter_parts.append(cel.equals("entity_id", entity_id)) + if entity_ids: + filter_parts.append(cel.in_("entity_id", entity_ids)) + if entity_type: + filter_parts.append(cel.equals("entity_type", entity_type.name.lower())) + if entity_types: + filter_parts.append(cel.in_("entity_type", [et.name.lower() for et in entity_types])) + query_filter = cel.and_(*filter_parts) if filter_parts else None + remote_files = await self._low_level_client.list_all_remote_files(query_filter=query_filter) + return [self._apply_client_to_instance(rf) for rf in remote_files] + + async def update(self, remote_file: str | RemoteFile, update: RemoteFileUpdate | dict) -> RemoteFile: + """Update a remote file. + + Args: + remote_file: The RemoteFile or remote file ID to update. + update: Updates to apply to the RemoteFile. + + Returns: + The updated RemoteFile. + + """ + remote_file_id = remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file + if isinstance(update, dict): + update = RemoteFileUpdate.model_validate(update) + update._resource_id = remote_file_id + updated_remote_file = await self._low_level_client.update_remote_file(update=update) + return self._apply_client_to_instance(updated_remote_file) + + async def delete(self, remote_file: str | RemoteFile) -> None: + """Delete a RemoteFile. + + Args: + remote_file: The RemoteFile or remote file ID to delete. + """ + remote_file_id = remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file + await self._low_level_client.delete_remote_file(remote_file_id=remote_file_id) + + async def batch_delete(self, remote_files: list[str | RemoteFile]) -> None: + """Batch delete RemoteFiles. + + Args: + remote_files: The RemoteFiles or remote file IDs to delete. + """ + remote_file_ids = [remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file for remote_file in remote_files] + await self._low_level_client.batch_delete_remote_files(remote_file_ids=remote_file_ids) + + async def get_download_url(self, remote_file: str | RemoteFile) -> str: + """Get a download URL for a RemoteFile. + + Args: + remote_file: The RemoteFile or remote file ID to get the download URL for. + """ + remote_file_id = remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file + return await self._low_level_client.get_remote_file_download_url(remote_file_id=remote_file_id) \ No newline at end of file diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.py b/python/lib/sift_client/resources/sync_stubs/__init__.py index 6664a5e0f..bde4ca233 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.py +++ b/python/lib/sift_client/resources/sync_stubs/__init__.py @@ -8,6 +8,7 @@ CalculatedChannelsAPIAsync, ChannelsAPIAsync, PingAPIAsync, + RemoteFilesAPIAsync, ReportsAPIAsync, RulesAPIAsync, RunsAPIAsync, @@ -19,11 +20,22 @@ AssetsAPI = generate_sync_api(AssetsAPIAsync, "AssetsAPI") CalculatedChannelsAPI = generate_sync_api(CalculatedChannelsAPIAsync, "CalculatedChannelsAPI") ChannelsAPI = generate_sync_api(ChannelsAPIAsync, "ChannelsAPI") +RemoteFilesAPI = generate_sync_api(RemoteFilesAPIAsync, "RemoteFilesAPI") RulesAPI = generate_sync_api(RulesAPIAsync, "RulesAPI") RunsAPI = generate_sync_api(RunsAPIAsync, "RunsAPI") ReportsAPI = generate_sync_api(ReportsAPIAsync, "ReportsAPI") TagsAPI = generate_sync_api(TagsAPIAsync, "TagsAPI") - TestResultsAPI = generate_sync_api(TestResultsAPIAsync, "TestResultsAPI") -__all__ = ["AssetsAPI", "CalculatedChannelsAPI", "PingAPI", "ReportsAPI", "RunsAPI", "TagsAPI"] +__all__ = [ + "AssetsAPI", + "CalculatedChannelsAPI", + "ChannelsAPI", + "PingAPI", + "RemoteFilesAPI", + "ReportsAPI", + "RulesAPI", + "RunsAPI", + "TagsAPI", + "TestResultsAPI", +] diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index cf0ee1247..908f31a6a 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -18,6 +18,7 @@ from sift_client.sift_types.calculated_channel import ( CalculatedChannelUpdate, ) from sift_client.sift_types.channel import Channel +from sift_client.sift_types.remote_file import RemoteFile, RemoteFileEntityType, RemoteFileUpdate from sift_client.sift_types.report import Report, ReportUpdate from sift_client.sift_types.rule import Rule, RuleCreate, RuleUpdate from sift_client.sift_types.run import Run, RunCreate, RunUpdate @@ -524,6 +525,110 @@ class PingAPI: """ ... +class RemoteFilesAPI: + """Sync counterpart to `RemoteFilesAPIAsync`. + + High-level API for interacting with remote files. + + This class provides a Pythonic, notebook-friendly interface for interacting with the RemoteFilesAPI. + It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. + + All methods in this class use the RemoteFile class from the low-level wrapper, which is a user-friendly + representation of a remote file using standard Python data structures and types. + """ + + def __init__(self, sift_client: SiftClient): + """Initialize the RemoteFilesAPI. + + Args: + sift_client: The Sift client to use. + """ + ... + + def _run(self, coro): ... + def get(self, *, remote_file_id: str) -> RemoteFile: + """Get a RemoteFile. + + Args: + remote_file_id: The ID of the remote file. + + Returns: + The RemoteFile. + """ + ... + + def list_( + self, + *, + remote_file_id: str | None = None, + remote_file_ids: list[str] | None = None, + entity_id: str | None = None, + entity_ids: list[str] | None = None, + entity_type: RemoteFileEntityType | None = None, + entity_types: list[RemoteFileEntityType] | None = None, + name: str | None = None, + names: list[str] | None = None, + name_contains: str | None = None, + name_regex: str | re.Pattern | None = None, + ) -> list[RemoteFile]: + """List RemoteFiles. + + Args: + remote_file_id: The ID of the remote file. + remote_file_ids: List of remote file IDs. + entity_id: The entity ID. + entity_ids: List of entity IDs. + entity_type: The entity type. + entity_types: List of entity types. + name: The name of the file. + names: List of file names. + name_contains: String that the name should contain. + name_regex: Regex pattern for the name. + + Returns: + A list of RemoteFiles matching the filters. + """ + ... + + def update(self, remote_file: str | RemoteFile, update: RemoteFileUpdate | dict) -> RemoteFile: + """Update a remote file. + + Args: + remote_file: The RemoteFile or remote file ID to update. + update: Updates to apply to the RemoteFile. + + Returns: + The updated RemoteFile. + """ + ... + + def delete(self, remote_file: str | RemoteFile) -> None: + """Delete a RemoteFile. + + Args: + remote_file: The RemoteFile or remote file ID to delete. + """ + ... + + def batch_delete(self, remote_files: list[str | RemoteFile]) -> None: + """Batch delete RemoteFiles. + + Args: + remote_files: The RemoteFiles or remote file IDs to delete. + """ + ... + + def get_download_url(self, remote_file: str | RemoteFile) -> str: + """Get a download URL for a RemoteFile. + + Args: + remote_file: The RemoteFile or remote file ID to get the download URL for. + + Returns: + The download URL for the remote file. + """ + ... + class ReportsAPI: """Sync counterpart to `ReportsAPIAsync`. diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py new file mode 100644 index 000000000..e5ce87ea3 --- /dev/null +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from datetime import datetime, timezone +from enum import Enum + +from sift.remote_files.v1.remote_files_pb2 import RemoteFile as RemoteFileProto +from sift.remote_files.v1.remote_files_pb2 import EntityType +from sift_client.sift_types._base import BaseType, ModelUpdate + +if TYPE_CHECKING: + from sift_client.client import SiftClient + from sift_client.sift_types.run import Run + from sift_client.sift_types.annotation import Annotation + from sift_client.sift_types.asset import Asset + from sift_client.sift_types.annotation_log import AnnotationLog + from sift_client.sift_types.test_report import TestReport + + +class RemoteFileEntityType(Enum): + """Enum for the entity type of a remote file.""" + + UNSPECIFIED = EntityType.ENTITY_TYPE_UNSPECIFIED # 0 + RUNS = EntityType.ENTITY_TYPE_RUN # 1 + ANNOTATIONS = EntityType.ENTITY_TYPE_ANNOTATION # 2 + ASSETS = EntityType.ENTITY_TYPE_ASSET # 3 + ANNOTATION_LOGS = EntityType.ENTITY_TYPE_ANNOTATION_LOG # 4 + TEST_REPORTS = EntityType.ENTITY_TYPE_TEST_REPORT # 5 + + @classmethod + def from_str(cls, val: str) -> RemoteFileEntityType | None: + """Convert string representation to RemoteFileEntityType.""" + if isinstance(val, str) and val.startswith("ENTITY_TYPE_"): + for item in cls: + if "ENTITY_TYPE_" + item.name == val: + return item + + return cls(int(val)) + + def __str__(self) -> str: + return self.name.lower() + + @staticmethod + def from_api_format(val: str) -> RemoteFileEntityType | None: + """Convert API format string to RemoteFileEntityType.""" + for item in RemoteFileEntityType: + if "ENTITY_TYPE_" + item.name == val: + return item + return None + + @staticmethod + def from_str(val: str) -> RemoteFileEntityType | None: + """Convert string representation to RemoteFileEntityType.""" + if isinstance(val, str) and val.startswith("ENTITY_TYPE_"): + return RemoteFileEntityType.from_api_format(val) + raise Exception(f"Unknown remote file entity type: {val}") + + @staticmethod + def from_proto_value(proto_value: int) -> RemoteFileEntityType: + """Convert protobuf int value to RemoteFileEntityType.""" + return RemoteFileEntityType(proto_value) + + +class RemoteFile(BaseType[RemoteFileProto, "RemoteFile"]): + """Model of the Sift RemoteFile.""" + + id_: str + organization_id: str + entity_id: str + entity_type: RemoteFileEntityType + file_name: str + file_mime_type: str + file_content_encoding: str + storage_key: str + file_size: int + description: str + created_by_user_id: str + modified_by_user_id: str + created_date: datetime + modified_date: datetime + + @classmethod + def _from_proto(cls, proto: RemoteFileProto, sift_client: SiftClient | None = None) -> RemoteFile: + return cls( + id_=proto.remote_file_id, + organization_id=proto.organization_id, + entity_id=proto.entity_id, + entity_type=RemoteFileEntityType.from_proto_value(proto.entity_type), + file_name=proto.file_name, + file_mime_type=proto.file_mime_type, + file_content_encoding=proto.file_content_encoding, + storage_key=proto.storage_key, + file_size=proto.file_size, + description=proto.description, + created_by_user_id=proto.created_by_user_id, + modified_by_user_id=proto.modified_by_user_id, + created_date=proto.created_date.ToDatetime(tzinfo=timezone.utc), + modified_date=proto.modified_date.ToDatetime(tzinfo=timezone.utc), + _client=sift_client, + ) + + @property + def entity(self) -> Run | Annotation | Asset | AnnotationLog | TestReport: + if self.entity_type == RemoteFileEntityType.RUN: + return self.client.runs.get(self.entity_id) + elif self.entity_type == RemoteFileEntityType.ANNOTATION: + return self.client.annotations.get(self.entity_id) + elif self.entity_type == RemoteFileEntityType.ASSET: + return self.client.assets.get(self.entity_id) + elif self.entity_type == RemoteFileEntityType.ANNOTATION_LOG: + return self.client.annotation_logs.get(self.entity_id) + elif self.entity_type == RemoteFileEntityType.TEST_REPORT: + return self.client.test_reports.get(self.entity_id) + else: + raise Exception(f"Unknown remote file entity type: {self.entity_type}") + + +class RemoteFileUpdate(ModelUpdate[RemoteFileProto]): + """Model of the RemoteFile fields that can be updated.""" + + description: str | None = None + + def _get_proto_class(self) -> type[RemoteFileProto]: + return RemoteFileProto + + def _add_resource_id_to_proto(self, proto_msg: RemoteFileProto): + if self._resource_id is None: + raise ValueError("Resource ID must be set before adding to proto") + proto_msg.remote_file_id = self._resource_id \ No newline at end of file diff --git a/python/lib/sift_client/util/util.py b/python/lib/sift_client/util/util.py index 143c04e52..6d7bd1ae7 100644 --- a/python/lib/sift_client/util/util.py +++ b/python/lib/sift_client/util/util.py @@ -9,6 +9,7 @@ ChannelsAPIAsync, IngestionAPIAsync, PingAPIAsync, + RemoteFilesAPIAsync, ReportsAPIAsync, RulesAPIAsync, RunsAPIAsync, @@ -35,6 +36,9 @@ class AsyncAPIs(NamedTuple): ingestion: IngestionAPIAsync """Instance of the Ingestion API for making asynchronous requests.""" + remote_files: RemoteFilesAPIAsync + """Instance of the Remote Files API for making asynchronous requests.""" + reports: ReportsAPIAsync """Instance of the Reports API for making asynchronous requests.""" From 127fdbf44896f0b822f28e3bf4b0df48517e135c Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 10:27:29 -0800 Subject: [PATCH 002/127] Clean up files --- .../low_level_wrappers/remote_files.py | 7 +------ .../lib/sift_client/resources/remote_files.py | 14 ++++++------- .../lib/sift_client/sift_types/remote_file.py | 21 +++++++------------ 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 0ef62a146..017450260 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -4,13 +4,8 @@ from sift.remote_files.v1.remote_files_pb2 import ( BatchDeleteRemoteFilesRequest, - BatchDeleteRemoteFilesResponse, - CreateRemoteFileRequest, - CreateRemoteFileResponse, DeleteRemoteFileRequest, - DeleteRemoteFileResponse, GetRemoteFileDownloadUrlRequest, - GetRemoteFileDownloadUrlResponse, GetRemoteFileRequest, GetRemoteFileResponse, ListRemoteFilesRequest, @@ -158,5 +153,5 @@ async def get_remote_file_download_url(self, remote_file_id: str) -> str: """ request = GetRemoteFileDownloadUrlRequest(remote_file_id=remote_file_id) response = await self._grpc_client.get_stub(RemoteFileServiceStub).GetRemoteFileDownloadUrl(request) - return cast("GetRemoteFileDownloadUrlResponse", response).download_url + return response.download_url diff --git a/python/lib/sift_client/resources/remote_files.py b/python/lib/sift_client/resources/remote_files.py index 752ca57bc..c40692754 100644 --- a/python/lib/sift_client/resources/remote_files.py +++ b/python/lib/sift_client/resources/remote_files.py @@ -4,14 +4,13 @@ from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient from sift_client.resources._base import ResourceBase -from sift_client.sift_types.remote_file import RemoteFileUpdate +from sift_client.sift_types.remote_file import RemoteFile, RemoteFileUpdate, RemoteFileEntityType 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.remote_file import RemoteFile, RemoteFileEntityType class RemoteFilesAPIAsync(ResourceBase): @@ -24,9 +23,8 @@ class RemoteFilesAPIAsync(ResourceBase): representation of a remote file using standard Python data structures and types. """ - def __init__(self, sift_client: SiftClient): - """ - Initialize the RemoteFilesAPI. + def __init__(self, sift_client: SiftClient): + """Initialize the RemoteFilesAPI. Args: sift_client: The Sift client to use. @@ -43,7 +41,7 @@ async def get( Args: remote_file_id: The ID of the remote file. - + Returns: The RemoteFile. """ @@ -77,7 +75,7 @@ async def list_( names: List of file names. name_contains: String that the name should contain. name_regex: Regex pattern for the name. - + Returns: A list of RemoteFiles matching the filters. """ @@ -145,4 +143,4 @@ async def get_download_url(self, remote_file: str | RemoteFile) -> str: remote_file: The RemoteFile or remote file ID to get the download URL for. """ remote_file_id = remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file - return await self._low_level_client.get_remote_file_download_url(remote_file_id=remote_file_id) \ No newline at end of file + return await self._low_level_client.get_remote_file_download_url(remote_file_id=remote_file_id) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index e5ce87ea3..a0c42b77d 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -1,19 +1,20 @@ from __future__ import annotations -from typing import TYPE_CHECKING from datetime import datetime, timezone from enum import Enum +from typing import TYPE_CHECKING -from sift.remote_files.v1.remote_files_pb2 import RemoteFile as RemoteFileProto from sift.remote_files.v1.remote_files_pb2 import EntityType +from sift.remote_files.v1.remote_files_pb2 import RemoteFile as RemoteFileProto + from sift_client.sift_types._base import BaseType, ModelUpdate if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.sift_types.run import Run from sift_client.sift_types.annotation import Annotation - from sift_client.sift_types.asset import Asset from sift_client.sift_types.annotation_log import AnnotationLog + from sift_client.sift_types.asset import Asset + from sift_client.sift_types.run import Run from sift_client.sift_types.test_report import TestReport @@ -34,7 +35,7 @@ def from_str(cls, val: str) -> RemoteFileEntityType | None: for item in cls: if "ENTITY_TYPE_" + item.name == val: return item - + return cls(int(val)) def __str__(self) -> str: @@ -48,13 +49,6 @@ def from_api_format(val: str) -> RemoteFileEntityType | None: return item return None - @staticmethod - def from_str(val: str) -> RemoteFileEntityType | None: - """Convert string representation to RemoteFileEntityType.""" - if isinstance(val, str) and val.startswith("ENTITY_TYPE_"): - return RemoteFileEntityType.from_api_format(val) - raise Exception(f"Unknown remote file entity type: {val}") - @staticmethod def from_proto_value(proto_value: int) -> RemoteFileEntityType: """Convert protobuf int value to RemoteFileEntityType.""" @@ -101,6 +95,7 @@ def _from_proto(cls, proto: RemoteFileProto, sift_client: SiftClient | None = No @property def entity(self) -> Run | Annotation | Asset | AnnotationLog | TestReport: + """Get the entity that this remote file is attached to.""" if self.entity_type == RemoteFileEntityType.RUN: return self.client.runs.get(self.entity_id) elif self.entity_type == RemoteFileEntityType.ANNOTATION: @@ -126,4 +121,4 @@ def _get_proto_class(self) -> type[RemoteFileProto]: def _add_resource_id_to_proto(self, proto_msg: RemoteFileProto): if self._resource_id is None: raise ValueError("Resource ID must be set before adding to proto") - proto_msg.remote_file_id = self._resource_id \ No newline at end of file + proto_msg.remote_file_id = self._resource_id From fcd7bdbe83e99a9347a2f2069e680b3ebeefe416 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 10:30:10 -0800 Subject: [PATCH 003/127] clean up --- python/lib/sift_client/resources/remote_files.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/resources/remote_files.py b/python/lib/sift_client/resources/remote_files.py index c40692754..f6239769f 100644 --- a/python/lib/sift_client/resources/remote_files.py +++ b/python/lib/sift_client/resources/remote_files.py @@ -4,7 +4,11 @@ from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient from sift_client.resources._base import ResourceBase -from sift_client.sift_types.remote_file import RemoteFile, RemoteFileUpdate, RemoteFileEntityType +from sift_client.sift_types.remote_file import ( + RemoteFile, + RemoteFileEntityType, + RemoteFileUpdate, +) from sift_client.util import cel_utils as cel if TYPE_CHECKING: From 4b886635225536b455c8198812e40d2dc72a0837 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 10:33:49 -0800 Subject: [PATCH 004/127] format --- .../low_level_wrappers/remote_files.py | 9 ++++--- .../lib/sift_client/resources/remote_files.py | 25 ++++++++++++++----- .../lib/sift_client/sift_types/remote_file.py | 4 ++- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 017450260..6108824f4 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -107,7 +107,9 @@ async def list_remote_files( request = ListRemoteFilesRequest(**request_kwargs) response = await self._grpc_client.get_stub(RemoteFileServiceStub).ListRemoteFiles(request) response = cast("ListRemoteFilesResponse", response) - return [RemoteFile._from_proto(rf) for rf in response.remote_files], response.next_page_token + return [ + RemoteFile._from_proto(rf) for rf in response.remote_files + ], response.next_page_token async def update_remote_file(self, update: RemoteFileUpdate) -> RemoteFile: """Update a remote file. @@ -152,6 +154,7 @@ async def get_remote_file_download_url(self, remote_file_id: str) -> str: The download URL for the remote file. """ request = GetRemoteFileDownloadUrlRequest(remote_file_id=remote_file_id) - response = await self._grpc_client.get_stub(RemoteFileServiceStub).GetRemoteFileDownloadUrl(request) + response = await self._grpc_client.get_stub(RemoteFileServiceStub).GetRemoteFileDownloadUrl( + request + ) return response.download_url - diff --git a/python/lib/sift_client/resources/remote_files.py b/python/lib/sift_client/resources/remote_files.py index f6239769f..197a63e49 100644 --- a/python/lib/sift_client/resources/remote_files.py +++ b/python/lib/sift_client/resources/remote_files.py @@ -104,7 +104,9 @@ async def list_( remote_files = await self._low_level_client.list_all_remote_files(query_filter=query_filter) return [self._apply_client_to_instance(rf) for rf in remote_files] - async def update(self, remote_file: str | RemoteFile, update: RemoteFileUpdate | dict) -> RemoteFile: + async def update( + self, remote_file: str | RemoteFile, update: RemoteFileUpdate | dict + ) -> RemoteFile: """Update a remote file. Args: @@ -115,7 +117,9 @@ async def update(self, remote_file: str | RemoteFile, update: RemoteFileUpdate | The updated RemoteFile. """ - remote_file_id = remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file + remote_file_id = ( + remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file + ) if isinstance(update, dict): update = RemoteFileUpdate.model_validate(update) update._resource_id = remote_file_id @@ -128,7 +132,9 @@ async def delete(self, remote_file: str | RemoteFile) -> None: Args: remote_file: The RemoteFile or remote file ID to delete. """ - remote_file_id = remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file + remote_file_id = ( + remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file + ) await self._low_level_client.delete_remote_file(remote_file_id=remote_file_id) async def batch_delete(self, remote_files: list[str | RemoteFile]) -> None: @@ -137,7 +143,10 @@ async def batch_delete(self, remote_files: list[str | RemoteFile]) -> None: Args: remote_files: The RemoteFiles or remote file IDs to delete. """ - remote_file_ids = [remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file for remote_file in remote_files] + remote_file_ids = [ + remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file + for remote_file in remote_files + ] await self._low_level_client.batch_delete_remote_files(remote_file_ids=remote_file_ids) async def get_download_url(self, remote_file: str | RemoteFile) -> str: @@ -146,5 +155,9 @@ async def get_download_url(self, remote_file: str | RemoteFile) -> str: Args: remote_file: The RemoteFile or remote file ID to get the download URL for. """ - remote_file_id = remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file - return await self._low_level_client.get_remote_file_download_url(remote_file_id=remote_file_id) + remote_file_id = ( + remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file + ) + return await self._low_level_client.get_remote_file_download_url( + remote_file_id=remote_file_id + ) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index a0c42b77d..7c5a1abe7 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -74,7 +74,9 @@ class RemoteFile(BaseType[RemoteFileProto, "RemoteFile"]): modified_date: datetime @classmethod - def _from_proto(cls, proto: RemoteFileProto, sift_client: SiftClient | None = None) -> RemoteFile: + def _from_proto( + cls, proto: RemoteFileProto, sift_client: SiftClient | None = None + ) -> RemoteFile: return cls( id_=proto.remote_file_id, organization_id=proto.organization_id, From 24040a3617d4b8a5b093e753b9953c3915f3cf7f Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 10:51:53 -0800 Subject: [PATCH 005/127] clean up --- .../lib/sift_client/sift_types/remote_file.py | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index 7c5a1abe7..0366b379c 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -11,11 +11,8 @@ if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.sift_types.annotation import Annotation - from sift_client.sift_types.annotation_log import AnnotationLog from sift_client.sift_types.asset import Asset from sift_client.sift_types.run import Run - from sift_client.sift_types.test_report import TestReport class RemoteFileEntityType(Enum): @@ -96,20 +93,14 @@ def _from_proto( ) @property - def entity(self) -> Run | Annotation | Asset | AnnotationLog | TestReport: + def entity(self) -> Run | Annotation: """Get the entity that this remote file is attached to.""" - if self.entity_type == RemoteFileEntityType.RUN: - return self.client.runs.get(self.entity_id) - elif self.entity_type == RemoteFileEntityType.ANNOTATION: - return self.client.annotations.get(self.entity_id) - elif self.entity_type == RemoteFileEntityType.ASSET: - return self.client.assets.get(self.entity_id) - elif self.entity_type == RemoteFileEntityType.ANNOTATION_LOG: - return self.client.annotation_logs.get(self.entity_id) - elif self.entity_type == RemoteFileEntityType.TEST_REPORT: - return self.client.test_reports.get(self.entity_id) + if self.entity_type == RemoteFileEntityType.RUNS: + return self.client.runs.get(run_id=self.entity_id) + elif self.entity_type == RemoteFileEntityType.ASSETS: + return self.client.assets.get(asset_id=self.entity_id) else: - raise Exception(f"Unknown remote file entity type: {self.entity_type}") + raise Exception(f"Unknown or not implemented remote file entity type: {self.entity_type}") class RemoteFileUpdate(ModelUpdate[RemoteFileProto]): From b43b27970a0cceeb07a393a681b70c9dcacd535e Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 10:53:53 -0800 Subject: [PATCH 006/127] Annotation -> asset --- python/lib/sift_client/sift_types/remote_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index 0366b379c..d45594686 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -93,7 +93,7 @@ def _from_proto( ) @property - def entity(self) -> Run | Annotation: + def entity(self) -> Run | Asset: """Get the entity that this remote file is attached to.""" if self.entity_type == RemoteFileEntityType.RUNS: return self.client.runs.get(run_id=self.entity_id) From ce28cec205df0f20f764faee17f72a493f8e53b1 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 10:57:32 -0800 Subject: [PATCH 007/127] cleanup --- python/lib/sift_client/sift_types/remote_file.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index d45594686..7e1783d4d 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -100,7 +100,9 @@ def entity(self) -> Run | Asset: elif self.entity_type == RemoteFileEntityType.ASSETS: return self.client.assets.get(asset_id=self.entity_id) else: - raise Exception(f"Unknown or not implemented remote file entity type: {self.entity_type}") + raise Exception( + f"Unknown or not implemented remote file entity type: {self.entity_type}" + ) class RemoteFileUpdate(ModelUpdate[RemoteFileProto]): From b2156341b8334054de6a77742c5f60a9f80b234a Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 11:06:09 -0800 Subject: [PATCH 008/127] cleanup --- python/lib/sift_client/sift_types/remote_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index 7e1783d4d..91dd7bdf4 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -55,7 +55,6 @@ def from_proto_value(proto_value: int) -> RemoteFileEntityType: class RemoteFile(BaseType[RemoteFileProto, "RemoteFile"]): """Model of the Sift RemoteFile.""" - id_: str organization_id: str entity_id: str entity_type: RemoteFileEntityType From 28dc8c98b3dcabd42ba3bbf70abff2c03f402a0b Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 16:43:49 -0800 Subject: [PATCH 009/127] remote files has no high level api and must be accessed via their entity --- .../_tests/sift_types/test_asset.py | 60 +++++++ .../_tests/sift_types/test_results.py | 62 +++++++ .../sift_client/_tests/sift_types/test_run.py | 60 +++++++ python/lib/sift_client/client.py | 7 - python/lib/sift_client/resources/__init__.py | 4 - .../lib/sift_client/resources/remote_files.py | 163 ------------------ .../resources/sync_stubs/__init__.py | 3 - python/lib/sift_client/sift_types/asset.py | 34 ++++ .../lib/sift_client/sift_types/remote_file.py | 54 +++++- python/lib/sift_client/sift_types/run.py | 34 ++++ .../lib/sift_client/sift_types/test_report.py | 34 ++++ python/lib/sift_client/util/util.py | 4 - 12 files changed, 331 insertions(+), 188 deletions(-) delete mode 100644 python/lib/sift_client/resources/remote_files.py diff --git a/python/lib/sift_client/_tests/sift_types/test_asset.py b/python/lib/sift_client/_tests/sift_types/test_asset.py index e209e2aad..743bc261b 100644 --- a/python/lib/sift_client/_tests/sift_types/test_asset.py +++ b/python/lib/sift_client/_tests/sift_types/test_asset.py @@ -163,3 +163,63 @@ def test_update_calls_client_and_updates_self(self, mock_asset, mock_client): mock_update.assert_called_once_with(updated_asset) # Verify it returns self assert result is mock_asset + + @pytest.mark.asyncio + async def test_remote_files_property_fetches_files(self, mock_asset, mock_client): + """Test that remote_files property fetches files from low-level client.""" + from unittest.mock import AsyncMock, patch + + # Create mock remote files + mock_remote_file = MagicMock() + mock_remote_file.entity_id = mock_asset.id_ + mock_remote_files = [mock_remote_file] + + # Mock the low-level client + with patch('sift_client.sift_types.asset.RemoteFilesLowLevelClient') as MockLowLevelClient: + mock_low_level_instance = AsyncMock() + mock_low_level_instance.list_all_remote_files.return_value = mock_remote_files + MockLowLevelClient.return_value = mock_low_level_instance + + # Call remote_files property + result = await mock_asset.remote_files() + + # Verify low-level client was instantiated with grpc_client + MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + + # Verify list_all_remote_files was called with correct filter + mock_low_level_instance.list_all_remote_files.assert_called_once() + call_kwargs = mock_low_level_instance.list_all_remote_files.call_args.kwargs + assert 'query_filter' in call_kwargs + assert mock_asset.id_ in call_kwargs['query_filter'] + + # Verify result + assert result == mock_remote_files + + @pytest.mark.asyncio + async def test_remote_file_fetches_single_file(self, mock_asset, mock_client): + """Test that remote_file fetches a single file by ID from low-level client.""" + from unittest.mock import AsyncMock, patch + + # Create mock remote file + file_id = "remote_file_123" + mock_remote_file = MagicMock() + mock_remote_file.id_ = file_id + mock_remote_file.entity_id = mock_asset.id_ + + # Mock the low-level client + with patch('sift_client.sift_types.asset.RemoteFilesLowLevelClient') as MockLowLevelClient: + mock_low_level_instance = AsyncMock() + mock_low_level_instance.get_remote_file.return_value = mock_remote_file + MockLowLevelClient.return_value = mock_low_level_instance + + # Call remote_file method + result = await mock_asset.remote_file(file_id) + + # Verify low-level client was instantiated with grpc_client + MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + + # Verify get_remote_file was called with correct file_id + mock_low_level_instance.get_remote_file.assert_called_once_with(file_id) + + # Verify result + assert result == mock_remote_file diff --git a/python/lib/sift_client/_tests/sift_types/test_results.py b/python/lib/sift_client/_tests/sift_types/test_results.py index 2ae139a7e..94ffd7f9c 100644 --- a/python/lib/sift_client/_tests/sift_types/test_results.py +++ b/python/lib/sift_client/_tests/sift_types/test_results.py @@ -209,3 +209,65 @@ def test_step_measurements(self, mock_test_step, mock_test_measurement, mock_cli measurements = mock_test_step.measurements assert len(measurements) == 1 assert measurements[0] == mock_test_measurement + + @pytest.mark.asyncio + async def test_remote_files_property_fetches_files(self, mock_test_report, mock_client): + """Test that remote_files property fetches files from low-level client.""" + from unittest.mock import AsyncMock, patch + + # Create mock remote files + mock_remote_file = MagicMock() + mock_remote_file.entity_id = mock_test_report.id_ + mock_remote_files = [mock_remote_file] + + # Mock the low-level client + with patch('sift_client.sift_types.test_report.RemoteFilesLowLevelClient') as MockLowLevelClient: + mock_low_level_instance = AsyncMock() + mock_low_level_instance.list_all_remote_files.return_value = mock_remote_files + MockLowLevelClient.return_value = mock_low_level_instance + + # Call remote_files property + result = await mock_test_report.remote_files() + + # Verify low-level client was instantiated with grpc_client + MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + + # Verify list_all_remote_files was called with correct filter + mock_low_level_instance.list_all_remote_files.assert_called_once() + call_kwargs = mock_low_level_instance.list_all_remote_files.call_args.kwargs + assert 'query_filter' in call_kwargs + # Verify the filter contains both the test_report id and entity_type + assert mock_test_report.id_ in call_kwargs['query_filter'] + assert 'ENTITY_TYPE_TEST_REPORT' in call_kwargs['query_filter'] + + # Verify result + assert result == mock_remote_files + + @pytest.mark.asyncio + async def test_remote_file_fetches_single_file(self, mock_test_report, mock_client): + """Test that remote_file fetches a single file by ID from low-level client.""" + from unittest.mock import AsyncMock, patch + + # Create mock remote file + file_id = "remote_file_123" + mock_remote_file = MagicMock() + mock_remote_file.id_ = file_id + mock_remote_file.entity_id = mock_test_report.id_ + + # Mock the low-level client + with patch('sift_client.sift_types.test_report.RemoteFilesLowLevelClient') as MockLowLevelClient: + mock_low_level_instance = AsyncMock() + mock_low_level_instance.get_remote_file.return_value = mock_remote_file + MockLowLevelClient.return_value = mock_low_level_instance + + # Call remote_file method + result = await mock_test_report.remote_file(file_id) + + # Verify low-level client was instantiated with grpc_client + MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + + # Verify get_remote_file was called with correct file_id + mock_low_level_instance.get_remote_file.assert_called_once_with(file_id) + + # Verify result + assert result == mock_remote_file diff --git a/python/lib/sift_client/_tests/sift_types/test_run.py b/python/lib/sift_client/_tests/sift_types/test_run.py index 5f9e6b1a1..9debe36d8 100644 --- a/python/lib/sift_client/_tests/sift_types/test_run.py +++ b/python/lib/sift_client/_tests/sift_types/test_run.py @@ -205,3 +205,63 @@ def test_update_calls_client_and_updates_self(self, mock_run, mock_client): mock_update.assert_called_once_with(updated_run) # Verify it returns self assert result is mock_run + + @pytest.mark.asyncio + async def test_remote_files_property_fetches_files(self, mock_run, mock_client): + """Test that remote_files property fetches files from low-level client.""" + from unittest.mock import AsyncMock, patch + + # Create mock remote files + mock_remote_file = MagicMock() + mock_remote_file.entity_id = mock_run.id_ + mock_remote_files = [mock_remote_file] + + # Mock the low-level client + with patch('sift_client.sift_types.run.RemoteFilesLowLevelClient') as MockLowLevelClient: + mock_low_level_instance = AsyncMock() + mock_low_level_instance.list_all_remote_files.return_value = mock_remote_files + MockLowLevelClient.return_value = mock_low_level_instance + + # Call remote_files property + result = await mock_run.remote_files() + + # Verify low-level client was instantiated with grpc_client + MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + + # Verify list_all_remote_files was called with correct filter + mock_low_level_instance.list_all_remote_files.assert_called_once() + call_kwargs = mock_low_level_instance.list_all_remote_files.call_args.kwargs + assert 'query_filter' in call_kwargs + assert mock_run.id_ in call_kwargs['query_filter'] + + # Verify result + assert result == mock_remote_files + + @pytest.mark.asyncio + async def test_remote_file_fetches_single_file(self, mock_run, mock_client): + """Test that remote_file fetches a single file by ID from low-level client.""" + from unittest.mock import AsyncMock, patch + + # Create mock remote file + file_id = "remote_file_123" + mock_remote_file = MagicMock() + mock_remote_file.id_ = file_id + mock_remote_file.entity_id = mock_run.id_ + + # Mock the low-level client + with patch('sift_client.sift_types.run.RemoteFilesLowLevelClient') as MockLowLevelClient: + mock_low_level_instance = AsyncMock() + mock_low_level_instance.get_remote_file.return_value = mock_remote_file + MockLowLevelClient.return_value = mock_low_level_instance + + # Call remote_file method + result = await mock_run.remote_file(file_id) + + # Verify low-level client was instantiated with grpc_client + MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + + # Verify get_remote_file was called with correct file_id + mock_low_level_instance.get_remote_file.assert_called_once_with(file_id) + + # Verify result + assert result == mock_remote_file diff --git a/python/lib/sift_client/client.py b/python/lib/sift_client/client.py index 103c09738..2a2252ef8 100644 --- a/python/lib/sift_client/client.py +++ b/python/lib/sift_client/client.py @@ -11,8 +11,6 @@ IngestionAPIAsync, PingAPI, PingAPIAsync, - RemoteFilesAPI, - RemoteFilesAPIAsync, ReportsAPI, ReportsAPIAsync, RulesAPI, @@ -88,9 +86,6 @@ class SiftClient( ingestion: IngestionAPIAsync """Instance of the Ingestion API for making synchronous requests.""" - remote_files: RemoteFilesAPI - """Instance of the Remote Files API for making synchronous requests.""" - reports: ReportsAPI """Instance of the Reports API for making synchronous requests.""" @@ -146,7 +141,6 @@ def __init__( self.assets = AssetsAPI(self) self.calculated_channels = CalculatedChannelsAPI(self) self.channels = ChannelsAPI(self) - self.remote_files = RemoteFilesAPI(self) self.rules = RulesAPI(self) self.reports = ReportsAPI(self) self.runs = RunsAPI(self) @@ -159,7 +153,6 @@ def __init__( calculated_channels=CalculatedChannelsAPIAsync(self), channels=ChannelsAPIAsync(self), ingestion=IngestionAPIAsync(self), - remote_files=RemoteFilesAPIAsync(self), reports=ReportsAPIAsync(self), rules=RulesAPIAsync(self), runs=RunsAPIAsync(self), diff --git a/python/lib/sift_client/resources/__init__.py b/python/lib/sift_client/resources/__init__.py index 57b158037..968fabdb3 100644 --- a/python/lib/sift_client/resources/__init__.py +++ b/python/lib/sift_client/resources/__init__.py @@ -155,7 +155,6 @@ async def main(): from sift_client.resources.channels import ChannelsAPIAsync from sift_client.resources.ingestion import IngestionAPIAsync from sift_client.resources.ping import PingAPIAsync -from sift_client.resources.remote_files import RemoteFilesAPIAsync from sift_client.resources.reports import ReportsAPIAsync from sift_client.resources.rules import RulesAPIAsync from sift_client.resources.runs import RunsAPIAsync @@ -168,7 +167,6 @@ async def main(): CalculatedChannelsAPI, ChannelsAPI, PingAPI, - RemoteFilesAPI, ReportsAPI, RulesAPI, RunsAPI, @@ -186,8 +184,6 @@ async def main(): "IngestionAPIAsync", "PingAPI", "PingAPIAsync", - "RemoteFilesAPI", - "RemoteFilesAPIAsync", "ReportsAPI", "ReportsAPIAsync", "RulesAPI", diff --git a/python/lib/sift_client/resources/remote_files.py b/python/lib/sift_client/resources/remote_files.py deleted file mode 100644 index 197a63e49..000000000 --- a/python/lib/sift_client/resources/remote_files.py +++ /dev/null @@ -1,163 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient -from sift_client.resources._base import ResourceBase -from sift_client.sift_types.remote_file import ( - RemoteFile, - RemoteFileEntityType, - RemoteFileUpdate, -) -from sift_client.util import cel_utils as cel - -if TYPE_CHECKING: - import re - - from sift_client.client import SiftClient - - -class RemoteFilesAPIAsync(ResourceBase): - """High-level API for interacting with remote files. - - This class provides a Pythonic, notebook-friendly interface for interacting with the RemoteFilesAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. - - All methods in this class use the RemoteFile class from the low-level wrapper, which is a user-friendly - representation of a remote file using standard Python data structures and types. - """ - - def __init__(self, sift_client: SiftClient): - """Initialize the RemoteFilesAPI. - - Args: - sift_client: The Sift client to use. - """ - super().__init__(sift_client) - self._low_level_client = RemoteFilesLowLevelClient(grpc_client=self.client.grpc_client) - - async def get( - self, - *, - remote_file_id: str, - ) -> RemoteFile: - """Get a RemoteFile. - - Args: - remote_file_id: The ID of the remote file. - - Returns: - The RemoteFile. - """ - remote_file = await self._low_level_client.get_remote_file(remote_file_id=remote_file_id) - return self._apply_client_to_instance(remote_file) - - async def list_( - self, - *, - remote_file_id: str | None = None, - remote_file_ids: list[str] | None = None, - entity_id: str | None = None, - entity_ids: list[str] | None = None, - entity_type: RemoteFileEntityType | None = None, - entity_types: list[RemoteFileEntityType] | None = None, - name: str | None = None, - names: list[str] | None = None, - name_contains: str | None = None, - name_regex: str | re.Pattern | None = None, - ) -> list[RemoteFile]: - """List RemoteFiles. - - Args: - remote_file_id: The ID of the remote file. - remote_file_ids: List of remote file IDs. - entity_id: The entity ID. - entity_ids: List of entity IDs. - entity_type: The entity type. - entity_types: List of entity types. - name: The name of the file. - names: List of file names. - name_contains: String that the name should contain. - name_regex: Regex pattern for the name. - - Returns: - A list of RemoteFiles matching the filters. - """ - filter_parts = [ - *self._build_name_cel_filters( - name=name, names=names, name_contains=name_contains, name_regex=name_regex - ), - ] - if remote_file_id: - filter_parts.append(cel.equals("remote_file_id", remote_file_id)) - if remote_file_ids: - filter_parts.append(cel.in_("remote_file_id", remote_file_ids)) - if entity_id: - filter_parts.append(cel.equals("entity_id", entity_id)) - if entity_ids: - filter_parts.append(cel.in_("entity_id", entity_ids)) - if entity_type: - filter_parts.append(cel.equals("entity_type", entity_type.name.lower())) - if entity_types: - filter_parts.append(cel.in_("entity_type", [et.name.lower() for et in entity_types])) - query_filter = cel.and_(*filter_parts) if filter_parts else None - remote_files = await self._low_level_client.list_all_remote_files(query_filter=query_filter) - return [self._apply_client_to_instance(rf) for rf in remote_files] - - async def update( - self, remote_file: str | RemoteFile, update: RemoteFileUpdate | dict - ) -> RemoteFile: - """Update a remote file. - - Args: - remote_file: The RemoteFile or remote file ID to update. - update: Updates to apply to the RemoteFile. - - Returns: - The updated RemoteFile. - - """ - remote_file_id = ( - remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file - ) - if isinstance(update, dict): - update = RemoteFileUpdate.model_validate(update) - update._resource_id = remote_file_id - updated_remote_file = await self._low_level_client.update_remote_file(update=update) - return self._apply_client_to_instance(updated_remote_file) - - async def delete(self, remote_file: str | RemoteFile) -> None: - """Delete a RemoteFile. - - Args: - remote_file: The RemoteFile or remote file ID to delete. - """ - remote_file_id = ( - remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file - ) - await self._low_level_client.delete_remote_file(remote_file_id=remote_file_id) - - async def batch_delete(self, remote_files: list[str | RemoteFile]) -> None: - """Batch delete RemoteFiles. - - Args: - remote_files: The RemoteFiles or remote file IDs to delete. - """ - remote_file_ids = [ - remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file - for remote_file in remote_files - ] - await self._low_level_client.batch_delete_remote_files(remote_file_ids=remote_file_ids) - - async def get_download_url(self, remote_file: str | RemoteFile) -> str: - """Get a download URL for a RemoteFile. - - Args: - remote_file: The RemoteFile or remote file ID to get the download URL for. - """ - remote_file_id = ( - remote_file._id_or_error if isinstance(remote_file, RemoteFile) else remote_file - ) - return await self._low_level_client.get_remote_file_download_url( - remote_file_id=remote_file_id - ) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.py b/python/lib/sift_client/resources/sync_stubs/__init__.py index bde4ca233..412238b60 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.py +++ b/python/lib/sift_client/resources/sync_stubs/__init__.py @@ -8,7 +8,6 @@ CalculatedChannelsAPIAsync, ChannelsAPIAsync, PingAPIAsync, - RemoteFilesAPIAsync, ReportsAPIAsync, RulesAPIAsync, RunsAPIAsync, @@ -20,7 +19,6 @@ AssetsAPI = generate_sync_api(AssetsAPIAsync, "AssetsAPI") CalculatedChannelsAPI = generate_sync_api(CalculatedChannelsAPIAsync, "CalculatedChannelsAPI") ChannelsAPI = generate_sync_api(ChannelsAPIAsync, "ChannelsAPI") -RemoteFilesAPI = generate_sync_api(RemoteFilesAPIAsync, "RemoteFilesAPI") RulesAPI = generate_sync_api(RulesAPIAsync, "RulesAPI") RunsAPI = generate_sync_api(RunsAPIAsync, "RunsAPI") ReportsAPI = generate_sync_api(ReportsAPIAsync, "ReportsAPI") @@ -32,7 +30,6 @@ "CalculatedChannelsAPI", "ChannelsAPI", "PingAPI", - "RemoteFilesAPI", "ReportsAPI", "RulesAPI", "RunsAPI", diff --git a/python/lib/sift_client/sift_types/asset.py b/python/lib/sift_client/sift_types/asset.py index a0f088c7a..cacd3996f 100644 --- a/python/lib/sift_client/sift_types/asset.py +++ b/python/lib/sift_client/sift_types/asset.py @@ -12,6 +12,7 @@ if TYPE_CHECKING: from sift_client.client import SiftClient from sift_client.sift_types.channel import Channel + from sift_client.sift_types.remote_file import RemoteFile from sift_client.sift_types.run import Run @@ -88,6 +89,39 @@ def update(self, update: AssetUpdate | dict) -> Asset: self._update(updated_asset) return self + async def remote_files(self) -> list[RemoteFile]: + """Get the remote files associated with this asset. + + Returns: + A list of RemoteFile objects attached to this asset. + """ + from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + from sift_client.util import cel_utils as cel + + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) + + # Build CEL filter for entity_id and entity_type + filter_expr = cel.and_( + cel.equals("entity_id", self.id_), + cel.equals("entity_type", "ENTITY_TYPE_ASSET") + ) + + return await low_level_client.list_all_remote_files(query_filter=filter_expr) + + async def remote_file(self, file_id: str) -> RemoteFile: + """Get a specific remote file by ID. + + Args: + file_id: The ID of the remote file to retrieve. + + Returns: + The RemoteFile object. + """ + from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) + return await low_level_client.get_remote_file(file_id) + @classmethod def _from_proto(cls, proto: AssetProto, sift_client: SiftClient | None = None) -> Asset: return cls( diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index 91dd7bdf4..9dcba68a5 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -1,9 +1,12 @@ from __future__ import annotations +from pathlib import Path from datetime import datetime, timezone from enum import Enum from typing import TYPE_CHECKING +import requests + from sift.remote_files.v1.remote_files_pb2 import EntityType from sift.remote_files.v1.remote_files_pb2 import RemoteFile as RemoteFileProto @@ -13,6 +16,7 @@ from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset from sift_client.sift_types.run import Run + from sift_client.sift_types.test_report import TestReport class RemoteFileEntityType(Enum): @@ -92,17 +96,53 @@ def _from_proto( ) @property - def entity(self) -> Run | Asset: + def entity(self) -> Run | Asset | TestReport: """Get the entity that this remote file is attached to.""" - if self.entity_type == RemoteFileEntityType.RUNS: + if self.entity_type == RemoteFileEntityType.RUN: return self.client.runs.get(run_id=self.entity_id) - elif self.entity_type == RemoteFileEntityType.ASSETS: + elif self.entity_type == RemoteFileEntityType.ASSET: return self.client.assets.get(asset_id=self.entity_id) - else: - raise Exception( - f"Unknown or not implemented remote file entity type: {self.entity_type}" + elif self.entity_type == RemoteFileEntityType.TEST_REPORT: + return self.client.test_reports.get(test_report_id=self.entity_id) + elif self.entity_type in (RemoteFileEntityType.ANNOTATION, RemoteFileEntityType.ANNOTATION_LOG): + raise NotImplementedError( + f"Entity type {self.entity_type} is not yet supported for entity access" ) - + else: + raise Exception(f"Unknown remote file entity type: {self.entity_type}") + + def delete(self) -> None: + """Delete the remote file.""" + self.client.remote_files.delete(remote_file=self) + return self + + def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: + """Update the remote file.""" + updated_remote_file = self.client.remote_files.update(remote_file=self, update=update) + self._update(updated_remote_file) + return self + + def download_url(self) -> str: + """Get the download URL for the remote file.""" + return self.client.remote_files.get_download_url(remote_file=self) + + def download(self, output_path: str | Path) -> None: + """Download the remote file to a local path.""" + # Get the download URL + download_url = self.download_url() + + # Convert output_path to Path object for easier handling + output_path = Path(output_path) + + # Ensure the parent directory exists + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Download the file content + response = requests.get(download_url) + response.raise_for_status() + + # Write the content to the output file + output_path.write_bytes(response.content) class RemoteFileUpdate(ModelUpdate[RemoteFileProto]): """Model of the RemoteFile fields that can be updated.""" diff --git a/python/lib/sift_client/sift_types/run.py b/python/lib/sift_client/sift_types/run.py index 2e7816a5b..b6a65fc1d 100644 --- a/python/lib/sift_client/sift_types/run.py +++ b/python/lib/sift_client/sift_types/run.py @@ -20,6 +20,7 @@ if TYPE_CHECKING: from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset + from sift_client.sift_types.remote_file import RemoteFile class Run(BaseType[RunProto, "Run"]): @@ -117,6 +118,39 @@ def update(self, update: RunUpdate | dict) -> Run: self._update(updated_run) return self + async def remote_files(self) -> list[RemoteFile]: + """Get the remote files associated with this run. + + Returns: + A list of RemoteFile objects attached to this run. + """ + from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + from sift_client.util import cel_utils as cel + + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) + + # Build CEL filter for entity_id and entity_type + filter_expr = cel.and_( + cel.equals("entity_id", self.id_), + cel.equals("entity_type", "ENTITY_TYPE_RUN") + ) + + return await low_level_client.list_all_remote_files(query_filter=filter_expr) + + async def remote_file(self, file_id: str) -> RemoteFile: + """Get a specific remote file by ID. + + Args: + file_id: The ID of the remote file to retrieve. + + Returns: + The RemoteFile object. + """ + from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) + return await low_level_client.get_remote_file(file_id) + class RunBase(ModelCreateUpdateBase): """Base class for Run create and update models with shared fields and validation.""" diff --git a/python/lib/sift_client/sift_types/test_report.py b/python/lib/sift_client/sift_types/test_report.py index 74914332b..9a8f81302 100644 --- a/python/lib/sift_client/sift_types/test_report.py +++ b/python/lib/sift_client/sift_types/test_report.py @@ -37,6 +37,7 @@ if TYPE_CHECKING: from sift_client.client import SiftClient + from sift_client.sift_types.remote_file import RemoteFile class TestStatus(Enum): @@ -605,6 +606,39 @@ def unarchive(self) -> TestReport: self._update(updated_test_report) return self + async def remote_files(self) -> list[RemoteFile]: + """Get the remote files associated with this test report. + + Returns: + A list of RemoteFile objects attached to this test report. + """ + from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + from sift_client.util import cel_utils as cel + + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) + + # Build CEL filter for entity_id and entity_type + filter_expr = cel.and_( + cel.equals("entity_id", self.id_), + cel.equals("entity_type", "ENTITY_TYPE_TEST_REPORT") + ) + + return await low_level_client.list_all_remote_files(query_filter=filter_expr) + + async def remote_file(self, file_id: str) -> RemoteFile: + """Get a specific remote file by ID. + + Args: + file_id: The ID of the remote file to retrieve. + + Returns: + The RemoteFile object. + """ + from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) + return await low_level_client.get_remote_file(file_id) + @property def steps(self) -> list[TestStep]: # type: ignore """Get the TestSteps for the TestReport.""" diff --git a/python/lib/sift_client/util/util.py b/python/lib/sift_client/util/util.py index 6d7bd1ae7..143c04e52 100644 --- a/python/lib/sift_client/util/util.py +++ b/python/lib/sift_client/util/util.py @@ -9,7 +9,6 @@ ChannelsAPIAsync, IngestionAPIAsync, PingAPIAsync, - RemoteFilesAPIAsync, ReportsAPIAsync, RulesAPIAsync, RunsAPIAsync, @@ -36,9 +35,6 @@ class AsyncAPIs(NamedTuple): ingestion: IngestionAPIAsync """Instance of the Ingestion API for making asynchronous requests.""" - remote_files: RemoteFilesAPIAsync - """Instance of the Remote Files API for making asynchronous requests.""" - reports: ReportsAPIAsync """Instance of the Reports API for making asynchronous requests.""" From dde1af3293c9815b8b4882a3d719093430c9353c Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 16:46:10 -0800 Subject: [PATCH 010/127] lint --- .../_tests/sift_types/test_asset.py | 32 ++++++++-------- .../_tests/sift_types/test_results.py | 38 ++++++++++--------- .../sift_client/_tests/sift_types/test_run.py | 32 ++++++++-------- python/lib/sift_client/sift_types/asset.py | 19 +++++----- .../lib/sift_client/sift_types/remote_file.py | 16 +++++--- python/lib/sift_client/sift_types/run.py | 19 +++++----- .../lib/sift_client/sift_types/test_report.py | 19 +++++----- 7 files changed, 90 insertions(+), 85 deletions(-) diff --git a/python/lib/sift_client/_tests/sift_types/test_asset.py b/python/lib/sift_client/_tests/sift_types/test_asset.py index 743bc261b..8eb0905e2 100644 --- a/python/lib/sift_client/_tests/sift_types/test_asset.py +++ b/python/lib/sift_client/_tests/sift_types/test_asset.py @@ -168,30 +168,30 @@ def test_update_calls_client_and_updates_self(self, mock_asset, mock_client): async def test_remote_files_property_fetches_files(self, mock_asset, mock_client): """Test that remote_files property fetches files from low-level client.""" from unittest.mock import AsyncMock, patch - + # Create mock remote files mock_remote_file = MagicMock() mock_remote_file.entity_id = mock_asset.id_ mock_remote_files = [mock_remote_file] - + # Mock the low-level client - with patch('sift_client.sift_types.asset.RemoteFilesLowLevelClient') as MockLowLevelClient: + with patch("sift_client.sift_types.asset.RemoteFilesLowLevelClient") as MockLowLevelClient: mock_low_level_instance = AsyncMock() mock_low_level_instance.list_all_remote_files.return_value = mock_remote_files MockLowLevelClient.return_value = mock_low_level_instance - + # Call remote_files property result = await mock_asset.remote_files() - + # Verify low-level client was instantiated with grpc_client MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) - + # Verify list_all_remote_files was called with correct filter mock_low_level_instance.list_all_remote_files.assert_called_once() call_kwargs = mock_low_level_instance.list_all_remote_files.call_args.kwargs - assert 'query_filter' in call_kwargs - assert mock_asset.id_ in call_kwargs['query_filter'] - + assert "query_filter" in call_kwargs + assert mock_asset.id_ in call_kwargs["query_filter"] + # Verify result assert result == mock_remote_files @@ -199,27 +199,27 @@ async def test_remote_files_property_fetches_files(self, mock_asset, mock_client async def test_remote_file_fetches_single_file(self, mock_asset, mock_client): """Test that remote_file fetches a single file by ID from low-level client.""" from unittest.mock import AsyncMock, patch - + # Create mock remote file file_id = "remote_file_123" mock_remote_file = MagicMock() mock_remote_file.id_ = file_id mock_remote_file.entity_id = mock_asset.id_ - + # Mock the low-level client - with patch('sift_client.sift_types.asset.RemoteFilesLowLevelClient') as MockLowLevelClient: + with patch("sift_client.sift_types.asset.RemoteFilesLowLevelClient") as MockLowLevelClient: mock_low_level_instance = AsyncMock() mock_low_level_instance.get_remote_file.return_value = mock_remote_file MockLowLevelClient.return_value = mock_low_level_instance - + # Call remote_file method result = await mock_asset.remote_file(file_id) - + # Verify low-level client was instantiated with grpc_client MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) - + # Verify get_remote_file was called with correct file_id mock_low_level_instance.get_remote_file.assert_called_once_with(file_id) - + # Verify result assert result == mock_remote_file diff --git a/python/lib/sift_client/_tests/sift_types/test_results.py b/python/lib/sift_client/_tests/sift_types/test_results.py index 94ffd7f9c..05e67c566 100644 --- a/python/lib/sift_client/_tests/sift_types/test_results.py +++ b/python/lib/sift_client/_tests/sift_types/test_results.py @@ -214,32 +214,34 @@ def test_step_measurements(self, mock_test_step, mock_test_measurement, mock_cli async def test_remote_files_property_fetches_files(self, mock_test_report, mock_client): """Test that remote_files property fetches files from low-level client.""" from unittest.mock import AsyncMock, patch - + # Create mock remote files mock_remote_file = MagicMock() mock_remote_file.entity_id = mock_test_report.id_ mock_remote_files = [mock_remote_file] - + # Mock the low-level client - with patch('sift_client.sift_types.test_report.RemoteFilesLowLevelClient') as MockLowLevelClient: + with patch( + "sift_client.sift_types.test_report.RemoteFilesLowLevelClient" + ) as MockLowLevelClient: mock_low_level_instance = AsyncMock() mock_low_level_instance.list_all_remote_files.return_value = mock_remote_files MockLowLevelClient.return_value = mock_low_level_instance - + # Call remote_files property result = await mock_test_report.remote_files() - + # Verify low-level client was instantiated with grpc_client MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) - + # Verify list_all_remote_files was called with correct filter mock_low_level_instance.list_all_remote_files.assert_called_once() call_kwargs = mock_low_level_instance.list_all_remote_files.call_args.kwargs - assert 'query_filter' in call_kwargs + assert "query_filter" in call_kwargs # Verify the filter contains both the test_report id and entity_type - assert mock_test_report.id_ in call_kwargs['query_filter'] - assert 'ENTITY_TYPE_TEST_REPORT' in call_kwargs['query_filter'] - + assert mock_test_report.id_ in call_kwargs["query_filter"] + assert "ENTITY_TYPE_TEST_REPORT" in call_kwargs["query_filter"] + # Verify result assert result == mock_remote_files @@ -247,27 +249,29 @@ async def test_remote_files_property_fetches_files(self, mock_test_report, mock_ async def test_remote_file_fetches_single_file(self, mock_test_report, mock_client): """Test that remote_file fetches a single file by ID from low-level client.""" from unittest.mock import AsyncMock, patch - + # Create mock remote file file_id = "remote_file_123" mock_remote_file = MagicMock() mock_remote_file.id_ = file_id mock_remote_file.entity_id = mock_test_report.id_ - + # Mock the low-level client - with patch('sift_client.sift_types.test_report.RemoteFilesLowLevelClient') as MockLowLevelClient: + with patch( + "sift_client.sift_types.test_report.RemoteFilesLowLevelClient" + ) as MockLowLevelClient: mock_low_level_instance = AsyncMock() mock_low_level_instance.get_remote_file.return_value = mock_remote_file MockLowLevelClient.return_value = mock_low_level_instance - + # Call remote_file method result = await mock_test_report.remote_file(file_id) - + # Verify low-level client was instantiated with grpc_client MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) - + # Verify get_remote_file was called with correct file_id mock_low_level_instance.get_remote_file.assert_called_once_with(file_id) - + # Verify result assert result == mock_remote_file diff --git a/python/lib/sift_client/_tests/sift_types/test_run.py b/python/lib/sift_client/_tests/sift_types/test_run.py index 9debe36d8..17d180962 100644 --- a/python/lib/sift_client/_tests/sift_types/test_run.py +++ b/python/lib/sift_client/_tests/sift_types/test_run.py @@ -210,30 +210,30 @@ def test_update_calls_client_and_updates_self(self, mock_run, mock_client): async def test_remote_files_property_fetches_files(self, mock_run, mock_client): """Test that remote_files property fetches files from low-level client.""" from unittest.mock import AsyncMock, patch - + # Create mock remote files mock_remote_file = MagicMock() mock_remote_file.entity_id = mock_run.id_ mock_remote_files = [mock_remote_file] - + # Mock the low-level client - with patch('sift_client.sift_types.run.RemoteFilesLowLevelClient') as MockLowLevelClient: + with patch("sift_client.sift_types.run.RemoteFilesLowLevelClient") as MockLowLevelClient: mock_low_level_instance = AsyncMock() mock_low_level_instance.list_all_remote_files.return_value = mock_remote_files MockLowLevelClient.return_value = mock_low_level_instance - + # Call remote_files property result = await mock_run.remote_files() - + # Verify low-level client was instantiated with grpc_client MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) - + # Verify list_all_remote_files was called with correct filter mock_low_level_instance.list_all_remote_files.assert_called_once() call_kwargs = mock_low_level_instance.list_all_remote_files.call_args.kwargs - assert 'query_filter' in call_kwargs - assert mock_run.id_ in call_kwargs['query_filter'] - + assert "query_filter" in call_kwargs + assert mock_run.id_ in call_kwargs["query_filter"] + # Verify result assert result == mock_remote_files @@ -241,27 +241,27 @@ async def test_remote_files_property_fetches_files(self, mock_run, mock_client): async def test_remote_file_fetches_single_file(self, mock_run, mock_client): """Test that remote_file fetches a single file by ID from low-level client.""" from unittest.mock import AsyncMock, patch - + # Create mock remote file file_id = "remote_file_123" mock_remote_file = MagicMock() mock_remote_file.id_ = file_id mock_remote_file.entity_id = mock_run.id_ - + # Mock the low-level client - with patch('sift_client.sift_types.run.RemoteFilesLowLevelClient') as MockLowLevelClient: + with patch("sift_client.sift_types.run.RemoteFilesLowLevelClient") as MockLowLevelClient: mock_low_level_instance = AsyncMock() mock_low_level_instance.get_remote_file.return_value = mock_remote_file MockLowLevelClient.return_value = mock_low_level_instance - + # Call remote_file method result = await mock_run.remote_file(file_id) - + # Verify low-level client was instantiated with grpc_client MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) - + # Verify get_remote_file was called with correct file_id mock_low_level_instance.get_remote_file.assert_called_once_with(file_id) - + # Verify result assert result == mock_remote_file diff --git a/python/lib/sift_client/sift_types/asset.py b/python/lib/sift_client/sift_types/asset.py index cacd3996f..d42b0bb18 100644 --- a/python/lib/sift_client/sift_types/asset.py +++ b/python/lib/sift_client/sift_types/asset.py @@ -91,34 +91,33 @@ def update(self, update: AssetUpdate | dict) -> Asset: async def remote_files(self) -> list[RemoteFile]: """Get the remote files associated with this asset. - + Returns: A list of RemoteFile objects attached to this asset. """ from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient from sift_client.util import cel_utils as cel - + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) - + # Build CEL filter for entity_id and entity_type filter_expr = cel.and_( - cel.equals("entity_id", self.id_), - cel.equals("entity_type", "ENTITY_TYPE_ASSET") + cel.equals("entity_id", self.id_), cel.equals("entity_type", "ENTITY_TYPE_ASSET") ) - + return await low_level_client.list_all_remote_files(query_filter=filter_expr) - + async def remote_file(self, file_id: str) -> RemoteFile: """Get a specific remote file by ID. - + Args: file_id: The ID of the remote file to retrieve. - + Returns: The RemoteFile object. """ from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) return await low_level_client.get_remote_file(file_id) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index 9dcba68a5..3f3d09bb5 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -104,7 +104,10 @@ def entity(self) -> Run | Asset | TestReport: return self.client.assets.get(asset_id=self.entity_id) elif self.entity_type == RemoteFileEntityType.TEST_REPORT: return self.client.test_reports.get(test_report_id=self.entity_id) - elif self.entity_type in (RemoteFileEntityType.ANNOTATION, RemoteFileEntityType.ANNOTATION_LOG): + elif self.entity_type in ( + RemoteFileEntityType.ANNOTATION, + RemoteFileEntityType.ANNOTATION_LOG, + ): raise NotImplementedError( f"Entity type {self.entity_type} is not yet supported for entity access" ) @@ -115,7 +118,7 @@ def delete(self) -> None: """Delete the remote file.""" self.client.remote_files.delete(remote_file=self) return self - + def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: """Update the remote file.""" updated_remote_file = self.client.remote_files.update(remote_file=self, update=update) @@ -130,20 +133,21 @@ def download(self, output_path: str | Path) -> None: """Download the remote file to a local path.""" # Get the download URL download_url = self.download_url() - + # Convert output_path to Path object for easier handling output_path = Path(output_path) - + # Ensure the parent directory exists output_path.parent.mkdir(parents=True, exist_ok=True) - + # Download the file content response = requests.get(download_url) response.raise_for_status() - + # Write the content to the output file output_path.write_bytes(response.content) + class RemoteFileUpdate(ModelUpdate[RemoteFileProto]): """Model of the RemoteFile fields that can be updated.""" diff --git a/python/lib/sift_client/sift_types/run.py b/python/lib/sift_client/sift_types/run.py index b6a65fc1d..9ec03c68b 100644 --- a/python/lib/sift_client/sift_types/run.py +++ b/python/lib/sift_client/sift_types/run.py @@ -120,34 +120,33 @@ def update(self, update: RunUpdate | dict) -> Run: async def remote_files(self) -> list[RemoteFile]: """Get the remote files associated with this run. - + Returns: A list of RemoteFile objects attached to this run. """ from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient from sift_client.util import cel_utils as cel - + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) - + # Build CEL filter for entity_id and entity_type filter_expr = cel.and_( - cel.equals("entity_id", self.id_), - cel.equals("entity_type", "ENTITY_TYPE_RUN") + cel.equals("entity_id", self.id_), cel.equals("entity_type", "ENTITY_TYPE_RUN") ) - + return await low_level_client.list_all_remote_files(query_filter=filter_expr) - + async def remote_file(self, file_id: str) -> RemoteFile: """Get a specific remote file by ID. - + Args: file_id: The ID of the remote file to retrieve. - + Returns: The RemoteFile object. """ from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) return await low_level_client.get_remote_file(file_id) diff --git a/python/lib/sift_client/sift_types/test_report.py b/python/lib/sift_client/sift_types/test_report.py index 9a8f81302..84a26e8ef 100644 --- a/python/lib/sift_client/sift_types/test_report.py +++ b/python/lib/sift_client/sift_types/test_report.py @@ -608,34 +608,33 @@ def unarchive(self) -> TestReport: async def remote_files(self) -> list[RemoteFile]: """Get the remote files associated with this test report. - + Returns: A list of RemoteFile objects attached to this test report. """ from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient from sift_client.util import cel_utils as cel - + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) - + # Build CEL filter for entity_id and entity_type filter_expr = cel.and_( - cel.equals("entity_id", self.id_), - cel.equals("entity_type", "ENTITY_TYPE_TEST_REPORT") + cel.equals("entity_id", self.id_), cel.equals("entity_type", "ENTITY_TYPE_TEST_REPORT") ) - + return await low_level_client.list_all_remote_files(query_filter=filter_expr) - + async def remote_file(self, file_id: str) -> RemoteFile: """Get a specific remote file by ID. - + Args: file_id: The ID of the remote file to retrieve. - + Returns: The RemoteFile object. """ from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - + low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) return await low_level_client.get_remote_file(file_id) From afb2cd9986aede35b05f8400ddb59333b178075b Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 16:52:34 -0800 Subject: [PATCH 011/127] clean up --- .../_tests/sift_types/test_asset.py | 30 +++++++++++-------- .../_tests/sift_types/test_results.py | 24 +++++++-------- .../sift_client/_tests/sift_types/test_run.py | 26 ++++++++-------- .../lib/sift_client/sift_types/remote_file.py | 1 - 4 files changed, 42 insertions(+), 39 deletions(-) diff --git a/python/lib/sift_client/_tests/sift_types/test_asset.py b/python/lib/sift_client/_tests/sift_types/test_asset.py index 8eb0905e2..be62f26b2 100644 --- a/python/lib/sift_client/_tests/sift_types/test_asset.py +++ b/python/lib/sift_client/_tests/sift_types/test_asset.py @@ -175,20 +175,22 @@ async def test_remote_files_property_fetches_files(self, mock_asset, mock_client mock_remote_files = [mock_remote_file] # Mock the low-level client - with patch("sift_client.sift_types.asset.RemoteFilesLowLevelClient") as MockLowLevelClient: - mock_low_level_instance = AsyncMock() - mock_low_level_instance.list_all_remote_files.return_value = mock_remote_files - MockLowLevelClient.return_value = mock_low_level_instance + with patch( + "sift_client.sift_types.asset.RemoteFilesLowLevelClient" + ) as mock_low_level_client: + mock_low_level_client_instance = AsyncMock() + mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files + mock_low_level_client.return_value = mock_low_level_client_instance # Call remote_files property result = await mock_asset.remote_files() # Verify low-level client was instantiated with grpc_client - MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) # Verify list_all_remote_files was called with correct filter - mock_low_level_instance.list_all_remote_files.assert_called_once() - call_kwargs = mock_low_level_instance.list_all_remote_files.call_args.kwargs + mock_low_level_client_instance.list_all_remote_files.assert_called_once() + call_kwargs = mock_low_level_client_instance.list_all_remote_files.call_args.kwargs assert "query_filter" in call_kwargs assert mock_asset.id_ in call_kwargs["query_filter"] @@ -207,19 +209,21 @@ async def test_remote_file_fetches_single_file(self, mock_asset, mock_client): mock_remote_file.entity_id = mock_asset.id_ # Mock the low-level client - with patch("sift_client.sift_types.asset.RemoteFilesLowLevelClient") as MockLowLevelClient: - mock_low_level_instance = AsyncMock() - mock_low_level_instance.get_remote_file.return_value = mock_remote_file - MockLowLevelClient.return_value = mock_low_level_instance + with patch( + "sift_client.sift_types.asset.RemoteFilesLowLevelClient" + ) as mock_low_level_client: + mock_low_level_client_instance = AsyncMock() + mock_low_level_client_instance.get_remote_file.return_value = mock_remote_file + mock_low_level_client.return_value = mock_low_level_client_instance # Call remote_file method result = await mock_asset.remote_file(file_id) # Verify low-level client was instantiated with grpc_client - MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) # Verify get_remote_file was called with correct file_id - mock_low_level_instance.get_remote_file.assert_called_once_with(file_id) + mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) # Verify result assert result == mock_remote_file diff --git a/python/lib/sift_client/_tests/sift_types/test_results.py b/python/lib/sift_client/_tests/sift_types/test_results.py index 05e67c566..196c79159 100644 --- a/python/lib/sift_client/_tests/sift_types/test_results.py +++ b/python/lib/sift_client/_tests/sift_types/test_results.py @@ -223,20 +223,20 @@ async def test_remote_files_property_fetches_files(self, mock_test_report, mock_ # Mock the low-level client with patch( "sift_client.sift_types.test_report.RemoteFilesLowLevelClient" - ) as MockLowLevelClient: - mock_low_level_instance = AsyncMock() + ) as mock_low_level_client: + mock_low_level_client_instance = AsyncMock() mock_low_level_instance.list_all_remote_files.return_value = mock_remote_files - MockLowLevelClient.return_value = mock_low_level_instance + mock_low_level_client.return_value = mock_low_level_client_instance # Call remote_files property result = await mock_test_report.remote_files() # Verify low-level client was instantiated with grpc_client - MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) # Verify list_all_remote_files was called with correct filter - mock_low_level_instance.list_all_remote_files.assert_called_once() - call_kwargs = mock_low_level_instance.list_all_remote_files.call_args.kwargs + mock_low_level_client_instance.list_all_remote_files.assert_called_once() + call_kwargs = mock_low_level_client_instance.list_all_remote_files.call_args.kwargs assert "query_filter" in call_kwargs # Verify the filter contains both the test_report id and entity_type assert mock_test_report.id_ in call_kwargs["query_filter"] @@ -259,19 +259,19 @@ async def test_remote_file_fetches_single_file(self, mock_test_report, mock_clie # Mock the low-level client with patch( "sift_client.sift_types.test_report.RemoteFilesLowLevelClient" - ) as MockLowLevelClient: - mock_low_level_instance = AsyncMock() - mock_low_level_instance.get_remote_file.return_value = mock_remote_file - MockLowLevelClient.return_value = mock_low_level_instance + ) as mock_low_level_client: + mock_low_level_client_instance = AsyncMock() + mock_low_level_client_instance.get_remote_file.return_value = mock_remote_file + mock_low_level_client.return_value = mock_low_level_client_instance # Call remote_file method result = await mock_test_report.remote_file(file_id) # Verify low-level client was instantiated with grpc_client - MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) # Verify get_remote_file was called with correct file_id - mock_low_level_instance.get_remote_file.assert_called_once_with(file_id) + mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) # Verify result assert result == mock_remote_file diff --git a/python/lib/sift_client/_tests/sift_types/test_run.py b/python/lib/sift_client/_tests/sift_types/test_run.py index 17d180962..6fa9e2345 100644 --- a/python/lib/sift_client/_tests/sift_types/test_run.py +++ b/python/lib/sift_client/_tests/sift_types/test_run.py @@ -217,20 +217,20 @@ async def test_remote_files_property_fetches_files(self, mock_run, mock_client): mock_remote_files = [mock_remote_file] # Mock the low-level client - with patch("sift_client.sift_types.run.RemoteFilesLowLevelClient") as MockLowLevelClient: - mock_low_level_instance = AsyncMock() - mock_low_level_instance.list_all_remote_files.return_value = mock_remote_files - MockLowLevelClient.return_value = mock_low_level_instance + with patch("sift_client.sift_types.run.RemoteFilesLowLevelClient") as mock_low_level_client: + mock_low_level_client_instance = AsyncMock() + mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files + mock_low_level_client.return_value = mock_low_level_client_instance # Call remote_files property result = await mock_run.remote_files() # Verify low-level client was instantiated with grpc_client - MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) # Verify list_all_remote_files was called with correct filter - mock_low_level_instance.list_all_remote_files.assert_called_once() - call_kwargs = mock_low_level_instance.list_all_remote_files.call_args.kwargs + mock_low_level_client_instance.list_all_remote_files.assert_called_once() + call_kwargs = mock_low_level_client_instance.list_all_remote_files.call_args.kwargs assert "query_filter" in call_kwargs assert mock_run.id_ in call_kwargs["query_filter"] @@ -249,19 +249,19 @@ async def test_remote_file_fetches_single_file(self, mock_run, mock_client): mock_remote_file.entity_id = mock_run.id_ # Mock the low-level client - with patch("sift_client.sift_types.run.RemoteFilesLowLevelClient") as MockLowLevelClient: - mock_low_level_instance = AsyncMock() - mock_low_level_instance.get_remote_file.return_value = mock_remote_file - MockLowLevelClient.return_value = mock_low_level_instance + with patch("sift_client.sift_types.run.RemoteFilesLowLevelClient") as mock_low_level_client: + mock_low_level_client_instance = AsyncMock() + mock_low_level_client_instance.get_remote_file.return_value = mock_remote_file + mock_low_level_client.return_value = mock_low_level_client_instance # Call remote_file method result = await mock_run.remote_file(file_id) # Verify low-level client was instantiated with grpc_client - MockLowLevelClient.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) # Verify get_remote_file was called with correct file_id - mock_low_level_instance.get_remote_file.assert_called_once_with(file_id) + mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) # Verify result assert result == mock_remote_file diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index 3f3d09bb5..af4cdb39f 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -9,7 +9,6 @@ from sift.remote_files.v1.remote_files_pb2 import EntityType from sift.remote_files.v1.remote_files_pb2 import RemoteFile as RemoteFileProto - from sift_client.sift_types._base import BaseType, ModelUpdate if TYPE_CHECKING: From f7a57251af4c6c1a1c666fb93dc4f6230e3c906d Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 5 Nov 2025 16:56:06 -0800 Subject: [PATCH 012/127] clean up --- python/lib/sift_client/_tests/sift_types/test_results.py | 2 +- python/lib/sift_client/sift_types/remote_file.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/lib/sift_client/_tests/sift_types/test_results.py b/python/lib/sift_client/_tests/sift_types/test_results.py index 196c79159..0cd829b18 100644 --- a/python/lib/sift_client/_tests/sift_types/test_results.py +++ b/python/lib/sift_client/_tests/sift_types/test_results.py @@ -225,7 +225,7 @@ async def test_remote_files_property_fetches_files(self, mock_test_report, mock_ "sift_client.sift_types.test_report.RemoteFilesLowLevelClient" ) as mock_low_level_client: mock_low_level_client_instance = AsyncMock() - mock_low_level_instance.list_all_remote_files.return_value = mock_remote_files + mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files mock_low_level_client.return_value = mock_low_level_client_instance # Call remote_files property diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index af4cdb39f..ac68995c7 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -1,14 +1,14 @@ from __future__ import annotations -from pathlib import Path from datetime import datetime, timezone from enum import Enum +from pathlib import Path from typing import TYPE_CHECKING import requests - from sift.remote_files.v1.remote_files_pb2 import EntityType from sift.remote_files.v1.remote_files_pb2 import RemoteFile as RemoteFileProto + from sift_client.sift_types._base import BaseType, ModelUpdate if TYPE_CHECKING: From a77acef4e96e6f4f0e365cea6806ee583146e3ff Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 11:26:11 -0800 Subject: [PATCH 013/127] fix import cycle --- .../lib/sift_client/sift_types/remote_file.py | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index ac68995c7..443880db7 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio from datetime import datetime, timezone from enum import Enum from pathlib import Path @@ -97,15 +98,15 @@ def _from_proto( @property def entity(self) -> Run | Asset | TestReport: """Get the entity that this remote file is attached to.""" - if self.entity_type == RemoteFileEntityType.RUN: + if self.entity_type == RemoteFileEntityType.RUNS: return self.client.runs.get(run_id=self.entity_id) - elif self.entity_type == RemoteFileEntityType.ASSET: + elif self.entity_type == RemoteFileEntityType.ASSETS: return self.client.assets.get(asset_id=self.entity_id) - elif self.entity_type == RemoteFileEntityType.TEST_REPORT: - return self.client.test_reports.get(test_report_id=self.entity_id) + elif self.entity_type == RemoteFileEntityType.TEST_REPORTS: + return self.client.test_results.get(test_report_id=self.entity_id) elif self.entity_type in ( - RemoteFileEntityType.ANNOTATION, - RemoteFileEntityType.ANNOTATION_LOG, + RemoteFileEntityType.ANNOTATIONS, + RemoteFileEntityType.ANNOTATION_LOGS, ): raise NotImplementedError( f"Entity type {self.entity_type} is not yet supported for entity access" @@ -115,18 +116,43 @@ def entity(self) -> Run | Asset | TestReport: def delete(self) -> None: """Delete the remote file.""" - self.client.remote_files.delete(remote_file=self) - return self + if self.id_ is None: + raise ValueError("Remote file ID is not set") + from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + remote_files_client = RemoteFilesLowLevelClient(self.client.grpc_client) + loop = self.client.get_asyncio_loop() + asyncio.run_coroutine_threadsafe( + remote_files_client.delete_remote_file(remote_file_id=self.id_), + loop + ).result() def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: """Update the remote file.""" - updated_remote_file = self.client.remote_files.update(remote_file=self, update=update) - self._update(updated_remote_file) - return self + if isinstance(update, dict): + update = RemoteFileUpdate.model_validate(update) + if self.id_ is None: + raise ValueError("Remote file ID is not set") + update.resource_id = self.id_ + from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + remote_file_client = RemoteFilesLowLevelClient(self.client.grpc_client) + loop = self.client.get_asyncio_loop() + updated_remote_file = asyncio.run_coroutine_threadsafe( + remote_file_client.update_remote_file(update=update), + loop + ).result() + return updated_remote_file def download_url(self) -> str: """Get the download URL for the remote file.""" - return self.client.remote_files.get_download_url(remote_file=self) + if self.id_ is None: + raise ValueError("Remote file ID is not set") + from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + remote_files_client = RemoteFilesLowLevelClient(self.client.grpc_client) + loop = self.client.get_asyncio_loop() + return asyncio.run_coroutine_threadsafe( + remote_files_client.get_remote_file_download_url(remote_file_id=self.id_), + loop + ).result() def download(self, output_path: str | Path) -> None: """Download the remote file to a local path.""" From 669735e3b65dab630fd333ae89556606acfdc401 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 13:07:16 -0800 Subject: [PATCH 014/127] format --- python/lib/sift_client/sift_types/remote_file.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index 443880db7..017f7c3f6 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -119,11 +119,11 @@ def delete(self) -> None: if self.id_ is None: raise ValueError("Remote file ID is not set") from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + remote_files_client = RemoteFilesLowLevelClient(self.client.grpc_client) loop = self.client.get_asyncio_loop() asyncio.run_coroutine_threadsafe( - remote_files_client.delete_remote_file(remote_file_id=self.id_), - loop + remote_files_client.delete_remote_file(remote_file_id=self.id_), loop ).result() def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: @@ -134,11 +134,11 @@ def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: raise ValueError("Remote file ID is not set") update.resource_id = self.id_ from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + remote_file_client = RemoteFilesLowLevelClient(self.client.grpc_client) loop = self.client.get_asyncio_loop() updated_remote_file = asyncio.run_coroutine_threadsafe( - remote_file_client.update_remote_file(update=update), - loop + remote_file_client.update_remote_file(update=update), loop ).result() return updated_remote_file @@ -147,11 +147,11 @@ def download_url(self) -> str: if self.id_ is None: raise ValueError("Remote file ID is not set") from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + remote_files_client = RemoteFilesLowLevelClient(self.client.grpc_client) loop = self.client.get_asyncio_loop() return asyncio.run_coroutine_threadsafe( - remote_files_client.get_remote_file_download_url(remote_file_id=self.id_), - loop + remote_files_client.get_remote_file_download_url(remote_file_id=self.id_), loop ).result() def download(self, output_path: str | Path) -> None: From 50df4d01ba0de2c5cb44b34a5ef5790c2284c7da Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 13:22:42 -0800 Subject: [PATCH 015/127] cleanup --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 6108824f4..5e9f8e246 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -18,7 +18,7 @@ from sift_client._internal.low_level_wrappers.base import ( LowLevelClientBase, ) -from sift_client.sift_types.remote_file import RemoteFile, RemoteFileUpdate +from ...sift_types.remote_file import RemoteFile, RemoteFileUpdate from sift_client.transport import GrpcClient, WithGrpcClient From 350673ab82b7fb1e4dbaf5be560b09aa8e3a3b4e Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 13:27:41 -0800 Subject: [PATCH 016/127] cleanup --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 5e9f8e246..6108824f4 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -18,7 +18,7 @@ from sift_client._internal.low_level_wrappers.base import ( LowLevelClientBase, ) -from ...sift_types.remote_file import RemoteFile, RemoteFileUpdate +from sift_client.sift_types.remote_file import RemoteFile, RemoteFileUpdate from sift_client.transport import GrpcClient, WithGrpcClient From c5561e0363a76abc02a9e8b480e3cd462282e0c9 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 14:08:10 -0800 Subject: [PATCH 017/127] clean up --- python/lib/sift_client/sift_types/remote_file.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index 017f7c3f6..1b08d5501 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -128,12 +128,13 @@ def delete(self) -> None: def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: """Update the remote file.""" + from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient + if isinstance(update, dict): update = RemoteFileUpdate.model_validate(update) if self.id_ is None: raise ValueError("Remote file ID is not set") update.resource_id = self.id_ - from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient remote_file_client = RemoteFilesLowLevelClient(self.client.grpc_client) loop = self.client.get_asyncio_loop() From c57538781fe57816b21bdf5a73671f15d58d909a Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 14:21:33 -0800 Subject: [PATCH 018/127] cleanup --- python/lib/sift_client/sift_types/remote_file.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index 1b08d5501..a65691c88 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -9,6 +9,7 @@ import requests from sift.remote_files.v1.remote_files_pb2 import EntityType from sift.remote_files.v1.remote_files_pb2 import RemoteFile as RemoteFileProto +from typing_extensions import Self from sift_client.sift_types._base import BaseType, ModelUpdate @@ -126,7 +127,7 @@ def delete(self) -> None: remote_files_client.delete_remote_file(remote_file_id=self.id_), loop ).result() - def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: + def update(self, update: RemoteFileUpdate | dict) -> Self: """Update the remote file.""" from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient From a13a26f164707955e2d5a99fec608452dffbc745 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 14:27:51 -0800 Subject: [PATCH 019/127] cleanup --- python/lib/sift_client/sift_types/remote_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index a65691c88..4b8fe6f9f 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -9,7 +9,6 @@ import requests from sift.remote_files.v1.remote_files_pb2 import EntityType from sift.remote_files.v1.remote_files_pb2 import RemoteFile as RemoteFileProto -from typing_extensions import Self from sift_client.sift_types._base import BaseType, ModelUpdate @@ -127,7 +126,7 @@ def delete(self) -> None: remote_files_client.delete_remote_file(remote_file_id=self.id_), loop ).result() - def update(self, update: RemoteFileUpdate | dict) -> Self: + def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: """Update the remote file.""" from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient @@ -142,6 +141,7 @@ def update(self, update: RemoteFileUpdate | dict) -> Self: updated_remote_file = asyncio.run_coroutine_threadsafe( remote_file_client.update_remote_file(update=update), loop ).result() + updated_remote_file._client = self.client return updated_remote_file def download_url(self) -> str: From 35a63c0d40cb4eb346f22d311c1bb3f80c2b8111 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 14:32:32 -0800 Subject: [PATCH 020/127] sift client passed through low level remote files --- .../low_level_wrappers/remote_files.py | 27 ++++++++++++++----- .../lib/sift_client/sift_types/remote_file.py | 3 +-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 6108824f4..d113dd770 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from sift.remote_files.v1.remote_files_pb2 import ( BatchDeleteRemoteFilesRequest, @@ -21,6 +21,9 @@ from sift_client.sift_types.remote_file import RemoteFile, RemoteFileUpdate from sift_client.transport import GrpcClient, WithGrpcClient +if TYPE_CHECKING: + from sift_client.client import SiftClient + class RemoteFilesLowLevelClient(LowLevelClientBase, WithGrpcClient): """Low-level client for the RemoteFilesAPI. @@ -36,11 +39,14 @@ def __init__(self, grpc_client: GrpcClient): """ super().__init__(grpc_client) - async def get_remote_file(self, remote_file_id: str) -> RemoteFile: + async def get_remote_file( + self, remote_file_id: str, sift_client: SiftClient | None = None + ) -> RemoteFile: """Get a remote file by ID. Args: remote_file_id: The ID of the remote file to retrieve. + sift_client: The SiftClient to attach to the returned RemoteFile. Returns: The RemoteFile. @@ -48,7 +54,7 @@ async def get_remote_file(self, remote_file_id: str) -> RemoteFile: request = GetRemoteFileRequest(remote_file_id=remote_file_id) response = await self._grpc_client.get_stub(RemoteFileServiceStub).GetRemoteFile(request) grpc_remote_file = cast("GetRemoteFileResponse", response).remote_file - return RemoteFile._from_proto(grpc_remote_file) + return RemoteFile._from_proto(grpc_remote_file, sift_client) async def list_all_remote_files( self, @@ -56,6 +62,7 @@ async def list_all_remote_files( order_by: str | None = None, max_results: int | None = None, page_size: int | None = None, + sift_client: SiftClient | None = None, ) -> list[RemoteFile]: """List all remote files matching the given query. @@ -64,13 +71,14 @@ async def list_all_remote_files( order_by: The field to order by. max_results: The maximum number of results to return. page_size: The number of results to return per page. + sift_client: The SiftClient to attach to the returned RemoteFiles. Returns: A list of RemoteFiles matching the given query. """ return await self._handle_pagination( self.list_remote_files, - kwargs={"query_filter": query_filter}, + kwargs={"query_filter": query_filter, "sift_client": sift_client}, page_size=page_size, order_by=order_by, max_results=max_results, @@ -82,6 +90,7 @@ async def list_remote_files( page_token: str | None = None, query_filter: str | None = None, order_by: str | None = None, + sift_client: SiftClient | None = None, ) -> tuple[list[RemoteFile], str]: """List remote files with pagination support. @@ -90,6 +99,7 @@ async def list_remote_files( page_token: The page token for pagination. query_filter: The CEL query filter. order_by: The field to order by. + sift_client: The SiftClient to attach to the returned RemoteFiles. Returns: A tuple of (list of RemoteFiles, next_page_token). @@ -108,14 +118,17 @@ async def list_remote_files( response = await self._grpc_client.get_stub(RemoteFileServiceStub).ListRemoteFiles(request) response = cast("ListRemoteFilesResponse", response) return [ - RemoteFile._from_proto(rf) for rf in response.remote_files + RemoteFile._from_proto(rf, sift_client) for rf in response.remote_files ], response.next_page_token - async def update_remote_file(self, update: RemoteFileUpdate) -> RemoteFile: + async def update_remote_file( + self, update: RemoteFileUpdate, sift_client: SiftClient | None = None + ) -> RemoteFile: """Update a remote file. Args: update: The RemoteFileUpdate containing the fields to update. + sift_client: The SiftClient to attach to the returned RemoteFile. Returns: The updated RemoteFile. @@ -124,7 +137,7 @@ async def update_remote_file(self, update: RemoteFileUpdate) -> RemoteFile: request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask) response = await self._grpc_client.get_stub(RemoteFileServiceStub).UpdateRemoteFile(request) updated_grpc_remote_file = cast("UpdateRemoteFileResponse", response).remote_file - return RemoteFile._from_proto(updated_grpc_remote_file) + return RemoteFile._from_proto(updated_grpc_remote_file, sift_client) async def delete_remote_file(self, remote_file_id: str) -> None: """Delete a remote file. diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index 4b8fe6f9f..ac11778c5 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -139,9 +139,8 @@ def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: remote_file_client = RemoteFilesLowLevelClient(self.client.grpc_client) loop = self.client.get_asyncio_loop() updated_remote_file = asyncio.run_coroutine_threadsafe( - remote_file_client.update_remote_file(update=update), loop + remote_file_client.update_remote_file(update=update, sift_client=self.client), loop ).result() - updated_remote_file._client = self.client return updated_remote_file def download_url(self) -> str: From e16d500b3a0e368ff87e5f7485d4fc63cb475536 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 14:44:19 -0800 Subject: [PATCH 021/127] clean up --- .../_internal/low_level_wrappers/remote_files.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index d113dd770..683f3805c 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -18,11 +18,12 @@ from sift_client._internal.low_level_wrappers.base import ( LowLevelClientBase, ) -from sift_client.sift_types.remote_file import RemoteFile, RemoteFileUpdate +from sift_client.sift_types.remote_file import RemoteFileUpdate from sift_client.transport import GrpcClient, WithGrpcClient if TYPE_CHECKING: from sift_client.client import SiftClient + from sift_client.sift_types.remote_file import RemoteFile class RemoteFilesLowLevelClient(LowLevelClientBase, WithGrpcClient): @@ -51,6 +52,8 @@ async def get_remote_file( Returns: The RemoteFile. """ + from sift_client.sift_types.remote_file import RemoteFile + request = GetRemoteFileRequest(remote_file_id=remote_file_id) response = await self._grpc_client.get_stub(RemoteFileServiceStub).GetRemoteFile(request) grpc_remote_file = cast("GetRemoteFileResponse", response).remote_file @@ -104,6 +107,8 @@ async def list_remote_files( Returns: A tuple of (list of RemoteFiles, next_page_token). """ + from sift_client.sift_types.remote_file import RemoteFile + request_kwargs: dict[str, Any] = {} if page_size is not None: request_kwargs["page_size"] = page_size @@ -133,6 +138,8 @@ async def update_remote_file( Returns: The updated RemoteFile. """ + from sift_client.sift_types.remote_file import RemoteFile + grpc_remote_file, update_mask = update.to_proto_with_mask() request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask) response = await self._grpc_client.get_stub(RemoteFileServiceStub).UpdateRemoteFile(request) From bb88b8d00c380e55d207d4fda1c12b810572795a Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 14:49:03 -0800 Subject: [PATCH 022/127] cleanup --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 683f3805c..17b4afc78 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -13,7 +13,6 @@ UpdateRemoteFileRequest, UpdateRemoteFileResponse, ) -from sift.remote_files.v1.remote_files_pb2_grpc import RemoteFileServiceStub from sift_client._internal.low_level_wrappers.base import ( LowLevelClientBase, @@ -23,7 +22,7 @@ if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.sift_types.remote_file import RemoteFile + from sift_client.sift_types.remote_file import RemoteFile, RemoteFileServiceStub class RemoteFilesLowLevelClient(LowLevelClientBase, WithGrpcClient): From a40041651c13542e278d6baaf3c236db339c9d37 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 14:59:47 -0800 Subject: [PATCH 023/127] cleanup --- .../_internal/low_level_wrappers/remote_files.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 17b4afc78..f4c0ef9aa 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -13,16 +13,16 @@ UpdateRemoteFileRequest, UpdateRemoteFileResponse, ) +from sift.remote_files.v1.remote_files_pb2_grpc import RemoteFileServiceStub from sift_client._internal.low_level_wrappers.base import ( LowLevelClientBase, ) -from sift_client.sift_types.remote_file import RemoteFileUpdate from sift_client.transport import GrpcClient, WithGrpcClient if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.sift_types.remote_file import RemoteFile, RemoteFileServiceStub + from sift_client.sift_types.remote_file import RemoteFile, RemoteFileUpdate class RemoteFilesLowLevelClient(LowLevelClientBase, WithGrpcClient): @@ -137,7 +137,7 @@ async def update_remote_file( Returns: The updated RemoteFile. """ - from sift_client.sift_types.remote_file import RemoteFile + from sift_client.sift_types.remote_file import RemoteFile, RemoteFileUpdate grpc_remote_file, update_mask = update.to_proto_with_mask() request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask) From d977c45f5dfdfb7aead99135c46e534afff33a6a Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 15:04:01 -0800 Subject: [PATCH 024/127] cleanup --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index f4c0ef9aa..fda3a693a 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -137,7 +137,7 @@ async def update_remote_file( Returns: The updated RemoteFile. """ - from sift_client.sift_types.remote_file import RemoteFile, RemoteFileUpdate + from sift_client.sift_types.remote_file import RemoteFile grpc_remote_file, update_mask = update.to_proto_with_mask() request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask) From ebd53d9ba576acd58585e46e1c020fdfd7fd33bc Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 15:37:14 -0800 Subject: [PATCH 025/127] cleanup --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index fda3a693a..cbfd25938 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -137,13 +137,13 @@ async def update_remote_file( Returns: The updated RemoteFile. """ - from sift_client.sift_types.remote_file import RemoteFile + from sift_client.sift_types.remote_file import RemoteFile as RemoteFileClass grpc_remote_file, update_mask = update.to_proto_with_mask() request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask) response = await self._grpc_client.get_stub(RemoteFileServiceStub).UpdateRemoteFile(request) updated_grpc_remote_file = cast("UpdateRemoteFileResponse", response).remote_file - return RemoteFile._from_proto(updated_grpc_remote_file, sift_client) + return RemoteFileClass._from_proto(updated_grpc_remote_file, sift_client) async def delete_remote_file(self, remote_file_id: str) -> None: """Delete a remote file. From 21c2cd2dbdcf34d548f4c5d4eee078aa57c967ee Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 16:22:46 -0800 Subject: [PATCH 026/127] cleanup --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index cbfd25938..fda3a693a 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -137,13 +137,13 @@ async def update_remote_file( Returns: The updated RemoteFile. """ - from sift_client.sift_types.remote_file import RemoteFile as RemoteFileClass + from sift_client.sift_types.remote_file import RemoteFile grpc_remote_file, update_mask = update.to_proto_with_mask() request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask) response = await self._grpc_client.get_stub(RemoteFileServiceStub).UpdateRemoteFile(request) updated_grpc_remote_file = cast("UpdateRemoteFileResponse", response).remote_file - return RemoteFileClass._from_proto(updated_grpc_remote_file, sift_client) + return RemoteFile._from_proto(updated_grpc_remote_file, sift_client) async def delete_remote_file(self, remote_file_id: str) -> None: """Delete a remote file. From eae02aa99e48c740f290a2efc91d9fa74f10cc9a Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 16:33:38 -0800 Subject: [PATCH 027/127] cleanup --- python/lib/sift_client/sift_types/remote_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index ac11778c5..bf6011d2a 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -138,7 +138,7 @@ def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: remote_file_client = RemoteFilesLowLevelClient(self.client.grpc_client) loop = self.client.get_asyncio_loop() - updated_remote_file = asyncio.run_coroutine_threadsafe( + updated_remote_file: RemoteFile = asyncio.run_coroutine_threadsafe( remote_file_client.update_remote_file(update=update, sift_client=self.client), loop ).result() return updated_remote_file From 21b464dcecda82423b8d23e467e9bdb96913df04 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 16:43:22 -0800 Subject: [PATCH 028/127] cleanup --- python/lib/sift_client/sift_types/remote_file.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index bf6011d2a..dbdbbf7d0 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import requests from sift.remote_files.v1.remote_files_pb2 import EntityType @@ -138,9 +138,12 @@ def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: remote_file_client = RemoteFilesLowLevelClient(self.client.grpc_client) loop = self.client.get_asyncio_loop() - updated_remote_file: RemoteFile = asyncio.run_coroutine_threadsafe( - remote_file_client.update_remote_file(update=update, sift_client=self.client), loop - ).result() + updated_remote_file = cast( + RemoteFile, + asyncio.run_coroutine_threadsafe( + remote_file_client.update_remote_file(update=update, sift_client=self.client), loop + ).result(), + ) return updated_remote_file def download_url(self) -> str: From f99784899cf4aa80e038e1ff15ea6541bb1b4c85 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 16:47:11 -0800 Subject: [PATCH 029/127] cleanup --- python/lib/sift_client/sift_types/remote_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/remote_file.py index dbdbbf7d0..ed5437a6c 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/remote_file.py @@ -139,7 +139,7 @@ def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: remote_file_client = RemoteFilesLowLevelClient(self.client.grpc_client) loop = self.client.get_asyncio_loop() updated_remote_file = cast( - RemoteFile, + "RemoteFile", asyncio.run_coroutine_threadsafe( remote_file_client.update_remote_file(update=update, sift_client=self.client), loop ).result(), From 484bdaa84e733c5ed5e69c7434cc42d8c24f81bc Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 16:54:36 -0800 Subject: [PATCH 030/127] test fixes --- .../lib/sift_client/_tests/sift_types/test_asset.py | 13 ++++++++----- .../sift_client/_tests/sift_types/test_results.py | 12 +++++++----- .../lib/sift_client/_tests/sift_types/test_run.py | 13 ++++++++----- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/python/lib/sift_client/_tests/sift_types/test_asset.py b/python/lib/sift_client/_tests/sift_types/test_asset.py index be62f26b2..3a905dd00 100644 --- a/python/lib/sift_client/_tests/sift_types/test_asset.py +++ b/python/lib/sift_client/_tests/sift_types/test_asset.py @@ -182,17 +182,18 @@ async def test_remote_files_property_fetches_files(self, mock_asset, mock_client mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files mock_low_level_client.return_value = mock_low_level_client_instance - # Call remote_files property + # Call remote_files method result = await mock_asset.remote_files() # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(mock_client.grpc_client) # Verify list_all_remote_files was called with correct filter mock_low_level_client_instance.list_all_remote_files.assert_called_once() call_kwargs = mock_low_level_client_instance.list_all_remote_files.call_args.kwargs assert "query_filter" in call_kwargs assert mock_asset.id_ in call_kwargs["query_filter"] + assert "ENTITY_TYPE_ASSET" in call_kwargs["query_filter"] # Verify result assert result == mock_remote_files @@ -220,10 +221,12 @@ async def test_remote_file_fetches_single_file(self, mock_asset, mock_client): result = await mock_asset.remote_file(file_id) # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(mock_client.grpc_client) - # Verify get_remote_file was called with correct file_id - mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) + # Verify get_remote_file was called with correct file_id and sift_client + mock_low_level_client_instance.get_remote_file.assert_called_once_with( + file_id, sift_client=None + ) # Verify result assert result == mock_remote_file diff --git a/python/lib/sift_client/_tests/sift_types/test_results.py b/python/lib/sift_client/_tests/sift_types/test_results.py index 0cd829b18..b6fb7c872 100644 --- a/python/lib/sift_client/_tests/sift_types/test_results.py +++ b/python/lib/sift_client/_tests/sift_types/test_results.py @@ -228,11 +228,11 @@ async def test_remote_files_property_fetches_files(self, mock_test_report, mock_ mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files mock_low_level_client.return_value = mock_low_level_client_instance - # Call remote_files property + # Call remote_files method result = await mock_test_report.remote_files() # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(mock_client.grpc_client) # Verify list_all_remote_files was called with correct filter mock_low_level_client_instance.list_all_remote_files.assert_called_once() @@ -268,10 +268,12 @@ async def test_remote_file_fetches_single_file(self, mock_test_report, mock_clie result = await mock_test_report.remote_file(file_id) # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(mock_client.grpc_client) - # Verify get_remote_file was called with correct file_id - mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) + # Verify get_remote_file was called with correct file_id and sift_client + mock_low_level_client_instance.get_remote_file.assert_called_once_with( + file_id, sift_client=None + ) # Verify result assert result == mock_remote_file diff --git a/python/lib/sift_client/_tests/sift_types/test_run.py b/python/lib/sift_client/_tests/sift_types/test_run.py index 6741536ca..656e05f54 100644 --- a/python/lib/sift_client/_tests/sift_types/test_run.py +++ b/python/lib/sift_client/_tests/sift_types/test_run.py @@ -222,17 +222,18 @@ async def test_remote_files_property_fetches_files(self, mock_run, mock_client): mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files mock_low_level_client.return_value = mock_low_level_client_instance - # Call remote_files property + # Call remote_files method result = await mock_run.remote_files() # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(mock_client.grpc_client) # Verify list_all_remote_files was called with correct filter mock_low_level_client_instance.list_all_remote_files.assert_called_once() call_kwargs = mock_low_level_client_instance.list_all_remote_files.call_args.kwargs assert "query_filter" in call_kwargs assert mock_run.id_ in call_kwargs["query_filter"] + assert "ENTITY_TYPE_RUN" in call_kwargs["query_filter"] # Verify result assert result == mock_remote_files @@ -258,10 +259,12 @@ async def test_remote_file_fetches_single_file(self, mock_run, mock_client): result = await mock_run.remote_file(file_id) # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(grpc_client=mock_client.grpc_client) + mock_low_level_client.assert_called_once_with(mock_client.grpc_client) - # Verify get_remote_file was called with correct file_id - mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) + # Verify get_remote_file was called with correct file_id and sift_client + mock_low_level_client_instance.get_remote_file.assert_called_once_with( + file_id, sift_client=None + ) # Verify result assert result == mock_remote_file From 1951c449a9f70a112e76d18f8a5079a877a7fc8e Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 17:03:50 -0800 Subject: [PATCH 031/127] test fixes --- python/lib/sift_client/_tests/sift_types/test_asset.py | 4 ++-- python/lib/sift_client/_tests/sift_types/test_results.py | 4 ++-- python/lib/sift_client/_tests/sift_types/test_run.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/python/lib/sift_client/_tests/sift_types/test_asset.py b/python/lib/sift_client/_tests/sift_types/test_asset.py index 3a905dd00..ed600cf54 100644 --- a/python/lib/sift_client/_tests/sift_types/test_asset.py +++ b/python/lib/sift_client/_tests/sift_types/test_asset.py @@ -176,7 +176,7 @@ async def test_remote_files_property_fetches_files(self, mock_asset, mock_client # Mock the low-level client with patch( - "sift_client.sift_types.asset.RemoteFilesLowLevelClient" + "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" ) as mock_low_level_client: mock_low_level_client_instance = AsyncMock() mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files @@ -211,7 +211,7 @@ async def test_remote_file_fetches_single_file(self, mock_asset, mock_client): # Mock the low-level client with patch( - "sift_client.sift_types.asset.RemoteFilesLowLevelClient" + "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" ) as mock_low_level_client: mock_low_level_client_instance = AsyncMock() mock_low_level_client_instance.get_remote_file.return_value = mock_remote_file diff --git a/python/lib/sift_client/_tests/sift_types/test_results.py b/python/lib/sift_client/_tests/sift_types/test_results.py index b6fb7c872..d3fa51580 100644 --- a/python/lib/sift_client/_tests/sift_types/test_results.py +++ b/python/lib/sift_client/_tests/sift_types/test_results.py @@ -222,7 +222,7 @@ async def test_remote_files_property_fetches_files(self, mock_test_report, mock_ # Mock the low-level client with patch( - "sift_client.sift_types.test_report.RemoteFilesLowLevelClient" + "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" ) as mock_low_level_client: mock_low_level_client_instance = AsyncMock() mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files @@ -258,7 +258,7 @@ async def test_remote_file_fetches_single_file(self, mock_test_report, mock_clie # Mock the low-level client with patch( - "sift_client.sift_types.test_report.RemoteFilesLowLevelClient" + "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" ) as mock_low_level_client: mock_low_level_client_instance = AsyncMock() mock_low_level_client_instance.get_remote_file.return_value = mock_remote_file diff --git a/python/lib/sift_client/_tests/sift_types/test_run.py b/python/lib/sift_client/_tests/sift_types/test_run.py index 656e05f54..c3815c4b9 100644 --- a/python/lib/sift_client/_tests/sift_types/test_run.py +++ b/python/lib/sift_client/_tests/sift_types/test_run.py @@ -217,7 +217,7 @@ async def test_remote_files_property_fetches_files(self, mock_run, mock_client): mock_remote_files = [mock_remote_file] # Mock the low-level client - with patch("sift_client.sift_types.run.RemoteFilesLowLevelClient") as mock_low_level_client: + with patch("sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient") as mock_low_level_client: mock_low_level_client_instance = AsyncMock() mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files mock_low_level_client.return_value = mock_low_level_client_instance @@ -250,7 +250,7 @@ async def test_remote_file_fetches_single_file(self, mock_run, mock_client): mock_remote_file.entity_id = mock_run.id_ # Mock the low-level client - with patch("sift_client.sift_types.run.RemoteFilesLowLevelClient") as mock_low_level_client: + with patch("sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient") as mock_low_level_client: mock_low_level_client_instance = AsyncMock() mock_low_level_client_instance.get_remote_file.return_value = mock_remote_file mock_low_level_client.return_value = mock_low_level_client_instance From 7cc8f59fd5cfae06936b45fc163ce426a3632bbc Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 17:15:26 -0800 Subject: [PATCH 032/127] test fix --- python/lib/sift_client/_tests/sift_types/test_run.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/python/lib/sift_client/_tests/sift_types/test_run.py b/python/lib/sift_client/_tests/sift_types/test_run.py index c3815c4b9..59d046de4 100644 --- a/python/lib/sift_client/_tests/sift_types/test_run.py +++ b/python/lib/sift_client/_tests/sift_types/test_run.py @@ -217,7 +217,9 @@ async def test_remote_files_property_fetches_files(self, mock_run, mock_client): mock_remote_files = [mock_remote_file] # Mock the low-level client - with patch("sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient") as mock_low_level_client: + with patch( + "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" + ) as mock_low_level_client: mock_low_level_client_instance = AsyncMock() mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files mock_low_level_client.return_value = mock_low_level_client_instance @@ -250,7 +252,9 @@ async def test_remote_file_fetches_single_file(self, mock_run, mock_client): mock_remote_file.entity_id = mock_run.id_ # Mock the low-level client - with patch("sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient") as mock_low_level_client: + with patch( + "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" + ) as mock_low_level_client: mock_low_level_client_instance = AsyncMock() mock_low_level_client_instance.get_remote_file.return_value = mock_remote_file mock_low_level_client.return_value = mock_low_level_client_instance From 86eb7282a6bf2fc2e646c7139e3d57fec5dfeb34 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 17:54:37 -0800 Subject: [PATCH 033/127] test fixes --- python/lib/sift_client/_tests/sift_types/test_asset.py | 6 ++---- python/lib/sift_client/_tests/sift_types/test_results.py | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/python/lib/sift_client/_tests/sift_types/test_asset.py b/python/lib/sift_client/_tests/sift_types/test_asset.py index ed600cf54..a267a92a5 100644 --- a/python/lib/sift_client/_tests/sift_types/test_asset.py +++ b/python/lib/sift_client/_tests/sift_types/test_asset.py @@ -223,10 +223,8 @@ async def test_remote_file_fetches_single_file(self, mock_asset, mock_client): # Verify low-level client was instantiated with grpc_client mock_low_level_client.assert_called_once_with(mock_client.grpc_client) - # Verify get_remote_file was called with correct file_id and sift_client - mock_low_level_client_instance.get_remote_file.assert_called_once_with( - file_id, sift_client=None - ) + # Verify get_remote_file was called with correct file_id + mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) # Verify result assert result == mock_remote_file diff --git a/python/lib/sift_client/_tests/sift_types/test_results.py b/python/lib/sift_client/_tests/sift_types/test_results.py index d3fa51580..26ea70d44 100644 --- a/python/lib/sift_client/_tests/sift_types/test_results.py +++ b/python/lib/sift_client/_tests/sift_types/test_results.py @@ -270,10 +270,8 @@ async def test_remote_file_fetches_single_file(self, mock_test_report, mock_clie # Verify low-level client was instantiated with grpc_client mock_low_level_client.assert_called_once_with(mock_client.grpc_client) - # Verify get_remote_file was called with correct file_id and sift_client - mock_low_level_client_instance.get_remote_file.assert_called_once_with( - file_id, sift_client=None - ) + # Verify get_remote_file was called with correct file_id + mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) # Verify result assert result == mock_remote_file From 92f28824d63982b2de5deae0978946115413a2a2 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 18:02:40 -0800 Subject: [PATCH 034/127] test fixes --- python/lib/sift_client/_tests/sift_types/test_run.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/python/lib/sift_client/_tests/sift_types/test_run.py b/python/lib/sift_client/_tests/sift_types/test_run.py index 59d046de4..5a1ba140f 100644 --- a/python/lib/sift_client/_tests/sift_types/test_run.py +++ b/python/lib/sift_client/_tests/sift_types/test_run.py @@ -265,10 +265,8 @@ async def test_remote_file_fetches_single_file(self, mock_run, mock_client): # Verify low-level client was instantiated with grpc_client mock_low_level_client.assert_called_once_with(mock_client.grpc_client) - # Verify get_remote_file was called with correct file_id and sift_client - mock_low_level_client_instance.get_remote_file.assert_called_once_with( - file_id, sift_client=None - ) + # Verify get_remote_file was called with correct file_id + mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) # Verify result assert result == mock_remote_file From e44727ac36d2706fa29b3cb184cdcc65ab8eadf1 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 18:10:49 -0800 Subject: [PATCH 035/127] test fixes --- .../resources/sync_stubs/__init__.pyi | 104 ------------------ 1 file changed, 104 deletions(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 1583ff239..06aa33ed7 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -527,110 +527,6 @@ class PingAPI: """ ... -class RemoteFilesAPI: - """Sync counterpart to `RemoteFilesAPIAsync`. - - High-level API for interacting with remote files. - - This class provides a Pythonic, notebook-friendly interface for interacting with the RemoteFilesAPI. - It handles automatic handling of gRPC services, seamless type conversion, and clear error handling. - - All methods in this class use the RemoteFile class from the low-level wrapper, which is a user-friendly - representation of a remote file using standard Python data structures and types. - """ - - def __init__(self, sift_client: SiftClient): - """Initialize the RemoteFilesAPI. - - Args: - sift_client: The Sift client to use. - """ - ... - - def _run(self, coro): ... - def get(self, *, remote_file_id: str) -> RemoteFile: - """Get a RemoteFile. - - Args: - remote_file_id: The ID of the remote file. - - Returns: - The RemoteFile. - """ - ... - - def list_( - self, - *, - remote_file_id: str | None = None, - remote_file_ids: list[str] | None = None, - entity_id: str | None = None, - entity_ids: list[str] | None = None, - entity_type: RemoteFileEntityType | None = None, - entity_types: list[RemoteFileEntityType] | None = None, - name: str | None = None, - names: list[str] | None = None, - name_contains: str | None = None, - name_regex: str | re.Pattern | None = None, - ) -> list[RemoteFile]: - """List RemoteFiles. - - Args: - remote_file_id: The ID of the remote file. - remote_file_ids: List of remote file IDs. - entity_id: The entity ID. - entity_ids: List of entity IDs. - entity_type: The entity type. - entity_types: List of entity types. - name: The name of the file. - names: List of file names. - name_contains: String that the name should contain. - name_regex: Regex pattern for the name. - - Returns: - A list of RemoteFiles matching the filters. - """ - ... - - def update(self, remote_file: str | RemoteFile, update: RemoteFileUpdate | dict) -> RemoteFile: - """Update a remote file. - - Args: - remote_file: The RemoteFile or remote file ID to update. - update: Updates to apply to the RemoteFile. - - Returns: - The updated RemoteFile. - """ - ... - - def delete(self, remote_file: str | RemoteFile) -> None: - """Delete a RemoteFile. - - Args: - remote_file: The RemoteFile or remote file ID to delete. - """ - ... - - def batch_delete(self, remote_files: list[str | RemoteFile]) -> None: - """Batch delete RemoteFiles. - - Args: - remote_files: The RemoteFiles or remote file IDs to delete. - """ - ... - - def get_download_url(self, remote_file: str | RemoteFile) -> str: - """Get a download URL for a RemoteFile. - - Args: - remote_file: The RemoteFile or remote file ID to get the download URL for. - - Returns: - The download URL for the remote file. - """ - ... - class ReportsAPI: """Sync counterpart to `ReportsAPIAsync`. From 8217025e3eb3c0bc9066235b2f6da3dd32f65361 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 6 Nov 2025 18:13:55 -0800 Subject: [PATCH 036/127] test fixes --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 1 - 1 file changed, 1 deletion(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 06aa33ed7..d2f8a99e6 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -18,7 +18,6 @@ from sift_client.sift_types.calculated_channel import ( CalculatedChannelUpdate, ) from sift_client.sift_types.channel import Channel -from sift_client.sift_types.remote_file import RemoteFile, RemoteFileEntityType, RemoteFileUpdate from sift_client.sift_types.report import Report, ReportUpdate from sift_client.sift_types.rule import Rule, RuleCreate, RuleUpdate from sift_client.sift_types.run import Run, RunCreate, RunUpdate From 559797ab0501f0bcc3e153d8f211ccd33ddf4790 Mon Sep 17 00:00:00 2001 From: Alex Luck Date: Mon, 10 Nov 2025 11:48:16 -0800 Subject: [PATCH 037/127] Luck/python/remote files (#373) Co-authored-by: Andrew Baker --- .../low_level_wrappers/remote_files.py | 22 +++---- python/lib/sift_client/client.py | 4 ++ python/lib/sift_client/resources/__init__.py | 4 ++ .../sift_client/resources/file_attachments.py | 45 +++++++++++++ .../resources/sync_stubs/__init__.py | 3 + .../sift_types/_mixins/__init__.py | 0 .../sift_types/_mixins/file_attachments.py | 65 +++++++++++++++++++ python/lib/sift_client/sift_types/asset.py | 36 +--------- .../{remote_file.py => file_attachment.py} | 6 +- python/lib/sift_client/sift_types/run.py | 36 +--------- .../lib/sift_client/sift_types/test_report.py | 36 +--------- python/lib/sift_client/util/util.py | 4 ++ 12 files changed, 145 insertions(+), 116 deletions(-) create mode 100644 python/lib/sift_client/resources/file_attachments.py create mode 100644 python/lib/sift_client/sift_types/_mixins/__init__.py create mode 100644 python/lib/sift_client/sift_types/_mixins/file_attachments.py rename python/lib/sift_client/sift_types/{remote_file.py => file_attachment.py} (97%) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index fda3a693a..5d062aa97 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.sift_types.remote_file import RemoteFile, RemoteFileUpdate + from sift_client.sift_types.file_attachment import FileAttachment, RemoteFileUpdate class RemoteFilesLowLevelClient(LowLevelClientBase, WithGrpcClient): @@ -41,7 +41,7 @@ def __init__(self, grpc_client: GrpcClient): async def get_remote_file( self, remote_file_id: str, sift_client: SiftClient | None = None - ) -> RemoteFile: + ) -> FileAttachment: """Get a remote file by ID. Args: @@ -51,12 +51,12 @@ async def get_remote_file( Returns: The RemoteFile. """ - from sift_client.sift_types.remote_file import RemoteFile + from sift_client.sift_types.file_attachment import FileAttachment request = GetRemoteFileRequest(remote_file_id=remote_file_id) response = await self._grpc_client.get_stub(RemoteFileServiceStub).GetRemoteFile(request) grpc_remote_file = cast("GetRemoteFileResponse", response).remote_file - return RemoteFile._from_proto(grpc_remote_file, sift_client) + return FileAttachment._from_proto(grpc_remote_file, sift_client) async def list_all_remote_files( self, @@ -65,7 +65,7 @@ async def list_all_remote_files( max_results: int | None = None, page_size: int | None = None, sift_client: SiftClient | None = None, - ) -> list[RemoteFile]: + ) -> list[FileAttachment]: """List all remote files matching the given query. Args: @@ -93,7 +93,7 @@ async def list_remote_files( query_filter: str | None = None, order_by: str | None = None, sift_client: SiftClient | None = None, - ) -> tuple[list[RemoteFile], str]: + ) -> tuple[list[FileAttachment], str]: """List remote files with pagination support. Args: @@ -106,7 +106,7 @@ async def list_remote_files( Returns: A tuple of (list of RemoteFiles, next_page_token). """ - from sift_client.sift_types.remote_file import RemoteFile + from sift_client.sift_types.file_attachment import FileAttachment request_kwargs: dict[str, Any] = {} if page_size is not None: @@ -122,12 +122,12 @@ async def list_remote_files( response = await self._grpc_client.get_stub(RemoteFileServiceStub).ListRemoteFiles(request) response = cast("ListRemoteFilesResponse", response) return [ - RemoteFile._from_proto(rf, sift_client) for rf in response.remote_files + FileAttachment._from_proto(rf, sift_client) for rf in response.remote_files ], response.next_page_token async def update_remote_file( self, update: RemoteFileUpdate, sift_client: SiftClient | None = None - ) -> RemoteFile: + ) -> FileAttachment: """Update a remote file. Args: @@ -137,13 +137,13 @@ async def update_remote_file( Returns: The updated RemoteFile. """ - from sift_client.sift_types.remote_file import RemoteFile + from sift_client.sift_types.file_attachment import FileAttachment grpc_remote_file, update_mask = update.to_proto_with_mask() request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask) response = await self._grpc_client.get_stub(RemoteFileServiceStub).UpdateRemoteFile(request) updated_grpc_remote_file = cast("UpdateRemoteFileResponse", response).remote_file - return RemoteFile._from_proto(updated_grpc_remote_file, sift_client) + return FileAttachment._from_proto(updated_grpc_remote_file, sift_client) async def delete_remote_file(self, remote_file_id: str) -> None: """Delete a remote file. diff --git a/python/lib/sift_client/client.py b/python/lib/sift_client/client.py index 2a2252ef8..c269e499d 100644 --- a/python/lib/sift_client/client.py +++ b/python/lib/sift_client/client.py @@ -22,6 +22,7 @@ TestResultsAPI, TestResultsAPIAsync, ) +from sift_client.resources.sync_stubs import FileAttachmentsAPI from sift_client.transport import ( GrpcClient, GrpcConfig, @@ -146,6 +147,8 @@ def __init__( self.runs = RunsAPI(self) self.tags = TagsAPI(self) self.test_results = TestResultsAPI(self) + self.file_attachments = FileAttachmentsAPI(self) + # Accessor for the asynchronous APIs self.async_ = AsyncAPIs( ping=PingAPIAsync(self), @@ -158,6 +161,7 @@ def __init__( runs=RunsAPIAsync(self), tags=TagsAPIAsync(self), test_results=TestResultsAPIAsync(self), + file_attachments=FileAttachmentsAPIAsync(self), ) @property diff --git a/python/lib/sift_client/resources/__init__.py b/python/lib/sift_client/resources/__init__.py index 968fabdb3..0c8d856c6 100644 --- a/python/lib/sift_client/resources/__init__.py +++ b/python/lib/sift_client/resources/__init__.py @@ -160,6 +160,7 @@ async def main(): from sift_client.resources.runs import RunsAPIAsync from sift_client.resources.tags import TagsAPIAsync from sift_client.resources.test_results import TestResultsAPIAsync +from sift_client.resources.file_attachments import FileAttachmentsAPIAsync # ruff: noqa All imports needs to be imported before sync_stubs to avoid circular import from sift_client.resources.sync_stubs import ( @@ -172,6 +173,7 @@ async def main(): RunsAPI, TagsAPI, TestResultsAPI, + # FileAttachmentsAPI ) __all__ = [ @@ -194,4 +196,6 @@ async def main(): "TagsAPIAsync", "TestResultsAPI", "TestResultsAPIAsync", + "FileAttachmentsAPIAsync", + # "FileAttachmentsAPI" ] diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py new file mode 100644 index 000000000..331e0030e --- /dev/null +++ b/python/lib/sift_client/resources/file_attachments.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient +from sift_client.resources._base import ResourceBase + +if TYPE_CHECKING: + from sift_client.client import SiftClient + from sift_client.sift_types.file_attachment import FileAttachment + + +class FileAttachmentsAPIAsync(ResourceBase): + """High-level API for interacting with file attachments (remote files). + + 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. + """ + + def __init__(self, sift_client: SiftClient): + """Initialize the AssetsAPI. + + Args: + sift_client: The Sift client to use. + """ + super().__init__(sift_client) + self._low_level_client = RemoteFilesLowLevelClient(grpc_client=self.client.grpc_client) + + def get( + self, *, file_id: str | None = None, client_key: str | None = None + ) -> FileAttachment: ... + + def list_(self) -> list[FileAttachment]: ... + + def find(self) -> FileAttachment: ... + + def update(self) -> FileAttachment | list[FileAttachment]: ... + + def delete(self) -> None: ... + + def download(self, output_path: str | Path) -> None: ... diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.py b/python/lib/sift_client/resources/sync_stubs/__init__.py index 412238b60..cf3879278 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.py +++ b/python/lib/sift_client/resources/sync_stubs/__init__.py @@ -7,6 +7,7 @@ AssetsAPIAsync, CalculatedChannelsAPIAsync, ChannelsAPIAsync, + FileAttachmentsAPIAsync, PingAPIAsync, ReportsAPIAsync, RulesAPIAsync, @@ -24,11 +25,13 @@ ReportsAPI = generate_sync_api(ReportsAPIAsync, "ReportsAPI") TagsAPI = generate_sync_api(TagsAPIAsync, "TagsAPI") TestResultsAPI = generate_sync_api(TestResultsAPIAsync, "TestResultsAPI") +FileAttachmentsAPI = generate_sync_api(FileAttachmentsAPIAsync, "FileAttachmentsAPI") __all__ = [ "AssetsAPI", "CalculatedChannelsAPI", "ChannelsAPI", + "FileAttachmentsAPI", "PingAPI", "ReportsAPI", "RulesAPI", diff --git a/python/lib/sift_client/sift_types/_mixins/__init__.py b/python/lib/sift_client/sift_types/_mixins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py new file mode 100644 index 000000000..50a99076a --- /dev/null +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar + +if TYPE_CHECKING: + from sift_client.sift_types._base import BaseType + from sift_client.sift_types.file_attachment import FileAttachment + + +class FileAttachmentsMixin: + """Mixin for sift_types that support file attachments (remote files). + + This mixin assumes the class also inherits from BaseType, which provides: + - id_: str | None + - client: SiftClient property + + The entity type is automatically determined from the class name: + - Asset -> ENTITY_TYPE_ASSET + - Run -> ENTITY_TYPE_RUN + - TestReport -> ENTITY_TYPE_TEST_REPORT + """ + + # Mapping of class names to entity types + _ENTITY_TYPE_MAP: ClassVar[dict[str, str]] = { + "Asset": "ENTITY_TYPE_ASSET", + "Run": "ENTITY_TYPE_RUN", + "TestReport": "ENTITY_TYPE_TEST_REPORT", + } + + def _get_entity_type_name(self) -> str: + """Get the entity type for filtering based on the class name. + + Returns: + The entity type string (e.g., 'ENTITY_TYPE_ASSET', 'ENTITY_TYPE_RUN') + + Raises: + ValueError: If the class name is not in the entity type mapping. + """ + class_name = self.__class__.__name__ + entity_type = self._ENTITY_TYPE_MAP.get(class_name) + + if not entity_type: + raise ValueError( + f"{class_name} is not configured for attachments. " + f"Add it to FileAttachmentsMixin._ENTITY_TYPE_MAP" + ) + + return entity_type + + @property + def attachments(self: BaseType) -> list[FileAttachment]: + # Type ignore because mixin assumes BaseType attributes (client, id_) + return self.client.file_attachments.list( + entity_type=self._get_entity_type_name(), + entity_id=self.id_, + ) + + def delete_attachment( + self: BaseType, file_attachment: FileAttachment | list[FileAttachment] + ) -> FileAttachment: + return self.client.file_attachments.add( + entity_type=self._get_entity_type_name(), + entity=self.id_, + file_attachment=file_attachment, + ) diff --git a/python/lib/sift_client/sift_types/asset.py b/python/lib/sift_client/sift_types/asset.py index d42b0bb18..046dda452 100644 --- a/python/lib/sift_client/sift_types/asset.py +++ b/python/lib/sift_client/sift_types/asset.py @@ -6,17 +6,17 @@ from sift.assets.v1.assets_pb2 import Asset as AssetProto from sift_client.sift_types._base import BaseType, MappingHelper, ModelUpdate +from sift_client.sift_types._mixins.file_attachments import AttachmentsMixin from sift_client.sift_types.tag import Tag 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.remote_file import RemoteFile from sift_client.sift_types.run import Run -class Asset(BaseType[AssetProto, "Asset"]): +class Asset(BaseType[AssetProto, "Asset"], AttachmentsMixin): """Model of the Sift Asset.""" # Required fields @@ -89,38 +89,6 @@ def update(self, update: AssetUpdate | dict) -> Asset: self._update(updated_asset) return self - async def remote_files(self) -> list[RemoteFile]: - """Get the remote files associated with this asset. - - Returns: - A list of RemoteFile objects attached to this asset. - """ - from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - from sift_client.util import cel_utils as cel - - low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) - - # Build CEL filter for entity_id and entity_type - filter_expr = cel.and_( - cel.equals("entity_id", self.id_), cel.equals("entity_type", "ENTITY_TYPE_ASSET") - ) - - return await low_level_client.list_all_remote_files(query_filter=filter_expr) - - async def remote_file(self, file_id: str) -> RemoteFile: - """Get a specific remote file by ID. - - Args: - file_id: The ID of the remote file to retrieve. - - Returns: - The RemoteFile object. - """ - from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - - low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) - return await low_level_client.get_remote_file(file_id) - @classmethod def _from_proto(cls, proto: AssetProto, sift_client: SiftClient | None = None) -> Asset: return cls( diff --git a/python/lib/sift_client/sift_types/remote_file.py b/python/lib/sift_client/sift_types/file_attachment.py similarity index 97% rename from python/lib/sift_client/sift_types/remote_file.py rename to python/lib/sift_client/sift_types/file_attachment.py index ed5437a6c..7f4cc1667 100644 --- a/python/lib/sift_client/sift_types/remote_file.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -56,7 +56,7 @@ def from_proto_value(proto_value: int) -> RemoteFileEntityType: return RemoteFileEntityType(proto_value) -class RemoteFile(BaseType[RemoteFileProto, "RemoteFile"]): +class FileAttachment(BaseType[RemoteFileProto, "RemoteFile"]): """Model of the Sift RemoteFile.""" organization_id: str @@ -76,7 +76,7 @@ class RemoteFile(BaseType[RemoteFileProto, "RemoteFile"]): @classmethod def _from_proto( cls, proto: RemoteFileProto, sift_client: SiftClient | None = None - ) -> RemoteFile: + ) -> FileAttachment: return cls( id_=proto.remote_file_id, organization_id=proto.organization_id, @@ -126,7 +126,7 @@ def delete(self) -> None: remote_files_client.delete_remote_file(remote_file_id=self.id_), loop ).result() - def update(self, update: RemoteFileUpdate | dict) -> RemoteFile: + def update(self, update: RemoteFileUpdate | dict) -> FileAttachment: """Update the remote file.""" from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient diff --git a/python/lib/sift_client/sift_types/run.py b/python/lib/sift_client/sift_types/run.py index 9de400f77..0c687789b 100644 --- a/python/lib/sift_client/sift_types/run.py +++ b/python/lib/sift_client/sift_types/run.py @@ -14,16 +14,16 @@ ModelCreateUpdateBase, ModelUpdate, ) +from sift_client.sift_types._mixins.file_attachments import FileAttachmentsMixin from sift_client.sift_types.tag import Tag 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.asset import Asset - from sift_client.sift_types.remote_file import RemoteFile -class Run(BaseType[RunProto, "Run"]): +class Run(BaseType[RunProto, "Run"], FileAttachmentsMixin): """Run model representing a data collection run.""" # Required fields @@ -118,38 +118,6 @@ def update(self, update: RunUpdate | dict) -> Run: self._update(updated_run) return self - async def remote_files(self) -> list[RemoteFile]: - """Get the remote files associated with this run. - - Returns: - A list of RemoteFile objects attached to this run. - """ - from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - from sift_client.util import cel_utils as cel - - low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) - - # Build CEL filter for entity_id and entity_type - filter_expr = cel.and_( - cel.equals("entity_id", self.id_), cel.equals("entity_type", "ENTITY_TYPE_RUN") - ) - - return await low_level_client.list_all_remote_files(query_filter=filter_expr) - - async def remote_file(self, file_id: str) -> RemoteFile: - """Get a specific remote file by ID. - - Args: - file_id: The ID of the remote file to retrieve. - - Returns: - The RemoteFile object. - """ - from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - - low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) - return await low_level_client.get_remote_file(file_id) - def stop(self) -> Run: """Stop the run.""" self.client.runs.stop(run=self) diff --git a/python/lib/sift_client/sift_types/test_report.py b/python/lib/sift_client/sift_types/test_report.py index 84a26e8ef..17f384101 100644 --- a/python/lib/sift_client/sift_types/test_report.py +++ b/python/lib/sift_client/sift_types/test_report.py @@ -33,11 +33,11 @@ ModelCreateUpdateBase, ModelUpdate, ) +from sift_client.sift_types._mixins.file_attachments import AttachmentsMixin 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.remote_file import RemoteFile class TestStatus(Enum): @@ -514,7 +514,7 @@ def _to_proto(self) -> ErrorInfoProto: ) -class TestReport(BaseType[TestReportProto, "TestReport"]): +class TestReport(BaseType[TestReportProto, "TestReport"], AttachmentsMixin): """TestReport model representing a test report.""" status: TestStatus @@ -606,38 +606,6 @@ def unarchive(self) -> TestReport: self._update(updated_test_report) return self - async def remote_files(self) -> list[RemoteFile]: - """Get the remote files associated with this test report. - - Returns: - A list of RemoteFile objects attached to this test report. - """ - from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - from sift_client.util import cel_utils as cel - - low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) - - # Build CEL filter for entity_id and entity_type - filter_expr = cel.and_( - cel.equals("entity_id", self.id_), cel.equals("entity_type", "ENTITY_TYPE_TEST_REPORT") - ) - - return await low_level_client.list_all_remote_files(query_filter=filter_expr) - - async def remote_file(self, file_id: str) -> RemoteFile: - """Get a specific remote file by ID. - - Args: - file_id: The ID of the remote file to retrieve. - - Returns: - The RemoteFile object. - """ - from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - - low_level_client = RemoteFilesLowLevelClient(self.client.grpc_client) - return await low_level_client.get_remote_file(file_id) - @property def steps(self) -> list[TestStep]: # type: ignore """Get the TestSteps for the TestReport.""" diff --git a/python/lib/sift_client/util/util.py b/python/lib/sift_client/util/util.py index 143c04e52..f808c8f2d 100644 --- a/python/lib/sift_client/util/util.py +++ b/python/lib/sift_client/util/util.py @@ -7,6 +7,7 @@ AssetsAPIAsync, CalculatedChannelsAPIAsync, ChannelsAPIAsync, + FileAttachmentsAPIAsync, IngestionAPIAsync, PingAPIAsync, ReportsAPIAsync, @@ -50,6 +51,9 @@ class AsyncAPIs(NamedTuple): test_results: TestResultsAPIAsync """Instance of the Test Results API for making asynchronous requests.""" + file_attachments: FileAttachmentsAPIAsync + """Instance of the File Attachments API for making asynchronous requests.""" + def count_non_none(*args: Any) -> int: """Count the number of non-none arguments.""" From dfba2235943c060e3306b42b945af6f0d7b3914f Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 10 Nov 2025 12:58:06 -0800 Subject: [PATCH 038/127] formatting --- python/lib/sift_client/client.py | 1 + python/lib/sift_client/resources/__init__.py | 4 +-- .../sift_client/resources/file_attachments.py | 27 ++++++++++++++----- .../sift_client/sift_types/file_attachment.py | 2 +- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/python/lib/sift_client/client.py b/python/lib/sift_client/client.py index c269e499d..f566188a7 100644 --- a/python/lib/sift_client/client.py +++ b/python/lib/sift_client/client.py @@ -8,6 +8,7 @@ CalculatedChannelsAPIAsync, ChannelsAPI, ChannelsAPIAsync, + FileAttachmentsAPIAsync, IngestionAPIAsync, PingAPI, PingAPIAsync, diff --git a/python/lib/sift_client/resources/__init__.py b/python/lib/sift_client/resources/__init__.py index 0c8d856c6..073255bdf 100644 --- a/python/lib/sift_client/resources/__init__.py +++ b/python/lib/sift_client/resources/__init__.py @@ -173,7 +173,7 @@ async def main(): RunsAPI, TagsAPI, TestResultsAPI, - # FileAttachmentsAPI + FileAttachmentsAPI, ) __all__ = [ @@ -196,6 +196,6 @@ async def main(): "TagsAPIAsync", "TestResultsAPI", "TestResultsAPIAsync", + "FileAttachmentsAPI", "FileAttachmentsAPIAsync", - # "FileAttachmentsAPI" ] diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 331e0030e..8764d72da 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -1,12 +1,13 @@ from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient from sift_client.resources._base import ResourceBase if TYPE_CHECKING: + from pathlib import Path + from sift_client.client import SiftClient from sift_client.sift_types.file_attachment import FileAttachment @@ -32,14 +33,26 @@ def __init__(self, sift_client: SiftClient): def get( self, *, file_id: str | None = None, client_key: str | None = None - ) -> FileAttachment: ... + ) -> FileAttachment: + """Get a file attachment by ID.""" + ... - def list_(self) -> list[FileAttachment]: ... + def list_(self) -> list[FileAttachment]: + """List all file attachments.""" + ... - def find(self) -> FileAttachment: ... + def find(self) -> FileAttachment: + """Find a file attachment by ID.""" + ... - def update(self) -> FileAttachment | list[FileAttachment]: ... + def update(self) -> FileAttachment | list[FileAttachment]: + """Update a file attachment.""" + ... - def delete(self) -> None: ... + def delete(self) -> None: + """Delete a file attachment.""" + ... - def download(self, output_path: str | Path) -> None: ... + def download(self, output_path: str | Path) -> None: + """Download a file attachment.""" + ... diff --git a/python/lib/sift_client/sift_types/file_attachment.py b/python/lib/sift_client/sift_types/file_attachment.py index 7f4cc1667..6ea281c1c 100644 --- a/python/lib/sift_client/sift_types/file_attachment.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -139,7 +139,7 @@ def update(self, update: RemoteFileUpdate | dict) -> FileAttachment: remote_file_client = RemoteFilesLowLevelClient(self.client.grpc_client) loop = self.client.get_asyncio_loop() updated_remote_file = cast( - "RemoteFile", + "FileAttachment", asyncio.run_coroutine_threadsafe( remote_file_client.update_remote_file(update=update, sift_client=self.client), loop ).result(), From 616251e6ea588022fadff751a865fa9102e4780d Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 10 Nov 2025 13:00:01 -0800 Subject: [PATCH 039/127] format --- python/lib/sift_client/resources/file_attachments.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 8764d72da..0c6a36936 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -31,9 +31,7 @@ def __init__(self, sift_client: SiftClient): super().__init__(sift_client) self._low_level_client = RemoteFilesLowLevelClient(grpc_client=self.client.grpc_client) - def get( - self, *, file_id: str | None = None, client_key: str | None = None - ) -> FileAttachment: + def get(self, *, file_id: str | None = None, client_key: str | None = None) -> FileAttachment: """Get a file attachment by ID.""" ... From 53a15a177ad4dc24abcc31cf69caa9bf43cd50fc Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 10 Nov 2025 13:29:53 -0800 Subject: [PATCH 040/127] cleanup --- python/lib/sift_client/sift_types/asset.py | 4 ++-- python/lib/sift_client/sift_types/test_report.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/lib/sift_client/sift_types/asset.py b/python/lib/sift_client/sift_types/asset.py index 046dda452..78217934f 100644 --- a/python/lib/sift_client/sift_types/asset.py +++ b/python/lib/sift_client/sift_types/asset.py @@ -6,7 +6,7 @@ from sift.assets.v1.assets_pb2 import Asset as AssetProto from sift_client.sift_types._base import BaseType, MappingHelper, ModelUpdate -from sift_client.sift_types._mixins.file_attachments import AttachmentsMixin +from sift_client.sift_types._mixins.file_attachments import FileAttachmentsMixin from sift_client.sift_types.tag import Tag from sift_client.util.metadata import metadata_dict_to_proto, metadata_proto_to_dict @@ -16,7 +16,7 @@ from sift_client.sift_types.run import Run -class Asset(BaseType[AssetProto, "Asset"], AttachmentsMixin): +class Asset(BaseType[AssetProto, "Asset"], FileAttachmentsMixin): """Model of the Sift Asset.""" # Required fields diff --git a/python/lib/sift_client/sift_types/test_report.py b/python/lib/sift_client/sift_types/test_report.py index 17f384101..c69a9ec40 100644 --- a/python/lib/sift_client/sift_types/test_report.py +++ b/python/lib/sift_client/sift_types/test_report.py @@ -33,7 +33,7 @@ ModelCreateUpdateBase, ModelUpdate, ) -from sift_client.sift_types._mixins.file_attachments import AttachmentsMixin +from sift_client.sift_types._mixins.file_attachments import FileAttachmentsMixin from sift_client.util.metadata import metadata_dict_to_proto, metadata_proto_to_dict if TYPE_CHECKING: @@ -514,7 +514,7 @@ def _to_proto(self) -> ErrorInfoProto: ) -class TestReport(BaseType[TestReportProto, "TestReport"], AttachmentsMixin): +class TestReport(BaseType[TestReportProto, "TestReport"], FileAttachmentsMixin): """TestReport model representing a test report.""" status: TestStatus From f7e901786c96eeaf1dd9b10517576f3d79e68ef0 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 10 Nov 2025 13:56:42 -0800 Subject: [PATCH 041/127] add file attachments api back in --- python/lib/sift_client/client.py | 9 +- python/lib/sift_client/resources/__init__.py | 6 +- .../sift_client/resources/file_attachments.py | 137 ++++++++++++++---- .../resources/sync_stubs/__init__.py | 2 +- .../sift_types/_mixins/file_attachments.py | 45 ++++-- .../sift_client/sift_types/file_attachment.py | 2 +- python/lib/sift_client/util/util.py | 6 +- 7 files changed, 157 insertions(+), 50 deletions(-) diff --git a/python/lib/sift_client/client.py b/python/lib/sift_client/client.py index f566188a7..655bd56e4 100644 --- a/python/lib/sift_client/client.py +++ b/python/lib/sift_client/client.py @@ -8,6 +8,7 @@ CalculatedChannelsAPIAsync, ChannelsAPI, ChannelsAPIAsync, + FileAttachmentsAPI, FileAttachmentsAPIAsync, IngestionAPIAsync, PingAPI, @@ -23,7 +24,6 @@ TestResultsAPI, TestResultsAPIAsync, ) -from sift_client.resources.sync_stubs import FileAttachmentsAPI from sift_client.transport import ( GrpcClient, GrpcConfig, @@ -85,6 +85,9 @@ class SiftClient( channels: ChannelsAPI """Instance of the Channels API for making synchronous requests.""" + file_attachments: FileAttachmentsAPI + """Instance of the File Attachments API for making synchronous requests.""" + ingestion: IngestionAPIAsync """Instance of the Ingestion API for making synchronous requests.""" @@ -143,12 +146,12 @@ def __init__( self.assets = AssetsAPI(self) self.calculated_channels = CalculatedChannelsAPI(self) self.channels = ChannelsAPI(self) + self.file_attachments = FileAttachmentsAPI(self) self.rules = RulesAPI(self) self.reports = ReportsAPI(self) self.runs = RunsAPI(self) self.tags = TagsAPI(self) self.test_results = TestResultsAPI(self) - self.file_attachments = FileAttachmentsAPI(self) # Accessor for the asynchronous APIs self.async_ = AsyncAPIs( @@ -156,13 +159,13 @@ def __init__( assets=AssetsAPIAsync(self), calculated_channels=CalculatedChannelsAPIAsync(self), channels=ChannelsAPIAsync(self), + file_attachments=FileAttachmentsAPIAsync(self), ingestion=IngestionAPIAsync(self), reports=ReportsAPIAsync(self), rules=RulesAPIAsync(self), runs=RunsAPIAsync(self), tags=TagsAPIAsync(self), test_results=TestResultsAPIAsync(self), - file_attachments=FileAttachmentsAPIAsync(self), ) @property diff --git a/python/lib/sift_client/resources/__init__.py b/python/lib/sift_client/resources/__init__.py index 073255bdf..e3cc3b21d 100644 --- a/python/lib/sift_client/resources/__init__.py +++ b/python/lib/sift_client/resources/__init__.py @@ -153,6 +153,7 @@ async def main(): from sift_client.resources.assets import AssetsAPIAsync from sift_client.resources.calculated_channels import CalculatedChannelsAPIAsync from sift_client.resources.channels import ChannelsAPIAsync +from sift_client.resources.file_attachments import FileAttachmentsAPIAsync from sift_client.resources.ingestion import IngestionAPIAsync from sift_client.resources.ping import PingAPIAsync from sift_client.resources.reports import ReportsAPIAsync @@ -160,7 +161,6 @@ async def main(): from sift_client.resources.runs import RunsAPIAsync from sift_client.resources.tags import TagsAPIAsync from sift_client.resources.test_results import TestResultsAPIAsync -from sift_client.resources.file_attachments import FileAttachmentsAPIAsync # ruff: noqa All imports needs to be imported before sync_stubs to avoid circular import from sift_client.resources.sync_stubs import ( @@ -183,6 +183,8 @@ async def main(): "CalculatedChannelsAPIAsync", "ChannelsAPI", "ChannelsAPIAsync", + "FileAttachmentsAPI", + "FileAttachmentsAPIAsync", "IngestionAPIAsync", "PingAPI", "PingAPIAsync", @@ -196,6 +198,4 @@ async def main(): "TagsAPIAsync", "TestResultsAPI", "TestResultsAPIAsync", - "FileAttachmentsAPI", - "FileAttachmentsAPIAsync", ] diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 0c6a36936..3ec481111 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -6,24 +6,19 @@ from sift_client.resources._base import ResourceBase if TYPE_CHECKING: - from pathlib import Path - from sift_client.client import SiftClient - from sift_client.sift_types.file_attachment import FileAttachment + from sift_client.sift_types.file_attachment import FileAttachment, RemoteFileUpdate class FileAttachmentsAPIAsync(ResourceBase): """High-level API for interacting with file attachments (remote files). - 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. + This class provides a Pythonic interface for managing file attachments + on Sift entities like runs, assets, and test reports. """ def __init__(self, sift_client: SiftClient): - """Initialize the AssetsAPI. + """Initialize the FileAttachmentsAPIAsync. Args: sift_client: The Sift client to use. @@ -31,26 +26,114 @@ def __init__(self, sift_client: SiftClient): super().__init__(sift_client) self._low_level_client = RemoteFilesLowLevelClient(grpc_client=self.client.grpc_client) - def get(self, *, file_id: str | None = None, client_key: str | None = None) -> FileAttachment: - """Get a file attachment by ID.""" - ... + async def get(self, *, file_attachment_id: str) -> FileAttachment: + """Get a file attachment by ID. + + Args: + file_attachment_id: The ID of the file attachment to retrieve. + + Returns: + The FileAttachment. + """ + file_attachment = await self._low_level_client.get_remote_file( + remote_file_id=file_attachment_id, + sift_client=self.client, + ) + return self._apply_client_to_instance(file_attachment) + + async def list_( + self, + *, + entity_type: str | None = None, + entity_id: str | None = None, + query_filter: str | None = None, + order_by: str | None = None, + limit: int | None = None, + page_size: int | None = None, + ) -> list[FileAttachment]: + """List file attachments with optional filtering. + + Args: + entity_type: Filter by entity type (e.g., 'ENTITY_TYPE_ASSET', 'ENTITY_TYPE_RUN'). + entity_id: Filter by entity ID. + query_filter: Optional CEL query filter. + order_by: The field to order by. + limit: Maximum number of results to return. + page_size: Number of results per page. + + Returns: + A list of FileAttachments. + """ + # Build the filter + filters = [] + if entity_type: + filters.append(f'entity_type=="{entity_type}"') + if entity_id: + filters.append(f'entity_id=="{entity_id}"') + if query_filter: + filters.append(query_filter) + + combined_filter = " && ".join(filters) if filters else None + + file_attachments = await self._low_level_client.list_all_remote_files( + query_filter=combined_filter, + order_by=order_by, + max_results=limit, + page_size=page_size, + sift_client=self.client, + ) + return [self._apply_client_to_instance(fa) for fa in file_attachments] + + async def update( + self, + *, + file_attachment: RemoteFileUpdate | dict, + ) -> FileAttachment: + """Update a file attachment. + + Args: + file_attachment: The RemoteFileUpdate with fields to update. + + Returns: + The updated FileAttachment. + """ + from sift_client.sift_types.file_attachment import RemoteFileUpdate + + if isinstance(file_attachment, dict): + file_attachment = RemoteFileUpdate.model_validate(file_attachment) + + updated = await self._low_level_client.update_remote_file( + update=file_attachment, + sift_client=self.client, + ) + return self._apply_client_to_instance(updated) + + async def delete(self, *, file_attachment_id: str) -> None: + """Delete a file attachment. + + Args: + file_attachment_id: The ID of the file attachment to delete. + """ + await self._low_level_client.delete_remote_file(remote_file_id=file_attachment_id) + + async def batch_delete(self, *, file_attachment_ids: list[str]) -> None: + """Batch delete multiple file attachments. - def list_(self) -> list[FileAttachment]: - """List all file attachments.""" - ... + Args: + file_attachment_ids: List of file attachment IDs to delete (up to 1000). + """ + await self._low_level_client.batch_delete_remote_files(remote_file_ids=file_attachment_ids) - def find(self) -> FileAttachment: - """Find a file attachment by ID.""" - ... + async def get_download_url(self, *, file_attachment_id: str) -> str: + """Get a download URL for a file attachment. - def update(self) -> FileAttachment | list[FileAttachment]: - """Update a file attachment.""" - ... + Args: + file_attachment_id: The ID of the file attachment. - def delete(self) -> None: - """Delete a file attachment.""" - ... + Returns: + The download URL for the file attachment. + """ + return await self._low_level_client.get_remote_file_download_url( + remote_file_id=file_attachment_id + ) - def download(self, output_path: str | Path) -> None: - """Download a file attachment.""" - ... diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.py b/python/lib/sift_client/resources/sync_stubs/__init__.py index cf3879278..ab988e7f2 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.py +++ b/python/lib/sift_client/resources/sync_stubs/__init__.py @@ -20,12 +20,12 @@ AssetsAPI = generate_sync_api(AssetsAPIAsync, "AssetsAPI") CalculatedChannelsAPI = generate_sync_api(CalculatedChannelsAPIAsync, "CalculatedChannelsAPI") ChannelsAPI = generate_sync_api(ChannelsAPIAsync, "ChannelsAPI") +FileAttachmentsAPI = generate_sync_api(FileAttachmentsAPIAsync, "FileAttachmentsAPI") RulesAPI = generate_sync_api(RulesAPIAsync, "RulesAPI") RunsAPI = generate_sync_api(RunsAPIAsync, "RunsAPI") ReportsAPI = generate_sync_api(ReportsAPIAsync, "ReportsAPI") TagsAPI = generate_sync_api(TagsAPIAsync, "TagsAPI") TestResultsAPI = generate_sync_api(TestResultsAPIAsync, "TestResultsAPI") -FileAttachmentsAPI = generate_sync_api(FileAttachmentsAPIAsync, "FileAttachmentsAPI") __all__ = [ "AssetsAPI", diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 50a99076a..d537a8ce0 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -1,12 +1,22 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING, ClassVar, Protocol if TYPE_CHECKING: - from sift_client.sift_types._base import BaseType + from sift_client.client import SiftClient from sift_client.sift_types.file_attachment import FileAttachment +class _SupportsFileAttachments(Protocol): + """Protocol for types that support file attachments.""" + + @property + def client(self) -> SiftClient: ... + + @property + def id_(self) -> str | None: ... + + class FileAttachmentsMixin: """Mixin for sift_types that support file attachments (remote files). @@ -48,18 +58,29 @@ def _get_entity_type_name(self) -> str: return entity_type @property - def attachments(self: BaseType) -> list[FileAttachment]: - # Type ignore because mixin assumes BaseType attributes (client, id_) + def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]: + """Get all file attachments for this entity. + + Returns: + A list of FileAttachments associated with this entity. + """ return self.client.file_attachments.list( - entity_type=self._get_entity_type_name(), + entity_type=self._get_entity_type_name(), # type: ignore[attr-defined] entity_id=self.id_, ) def delete_attachment( - self: BaseType, file_attachment: FileAttachment | list[FileAttachment] - ) -> FileAttachment: - return self.client.file_attachments.add( - entity_type=self._get_entity_type_name(), - entity=self.id_, - file_attachment=file_attachment, - ) + self: _SupportsFileAttachments, file_attachment: FileAttachment | list[FileAttachment] + ) -> None: + """Delete one or more file attachments. + + Args: + file_attachment: A single FileAttachment or list of FileAttachments to delete. + """ + if isinstance(file_attachment, list): + file_ids = [fa.id_ for fa in file_attachment if fa.id_] + if file_ids: + self.client.file_attachments.batch_delete(file_attachment_ids=file_ids) + else: + if file_attachment.id_: + self.client.file_attachments.delete(file_attachment_id=file_attachment.id_) diff --git a/python/lib/sift_client/sift_types/file_attachment.py b/python/lib/sift_client/sift_types/file_attachment.py index 6ea281c1c..81f127423 100644 --- a/python/lib/sift_client/sift_types/file_attachment.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -56,7 +56,7 @@ def from_proto_value(proto_value: int) -> RemoteFileEntityType: return RemoteFileEntityType(proto_value) -class FileAttachment(BaseType[RemoteFileProto, "RemoteFile"]): +class FileAttachment(BaseType[RemoteFileProto, "FileAttachment"]): """Model of the Sift RemoteFile.""" organization_id: str diff --git a/python/lib/sift_client/util/util.py b/python/lib/sift_client/util/util.py index f808c8f2d..60b58501b 100644 --- a/python/lib/sift_client/util/util.py +++ b/python/lib/sift_client/util/util.py @@ -33,6 +33,9 @@ class AsyncAPIs(NamedTuple): channels: ChannelsAPIAsync """Instance of the Channels API for making asynchronous requests.""" + file_attachments: FileAttachmentsAPIAsync + """Instance of the File Attachments API for making asynchronous requests.""" + ingestion: IngestionAPIAsync """Instance of the Ingestion API for making asynchronous requests.""" @@ -51,9 +54,6 @@ class AsyncAPIs(NamedTuple): test_results: TestResultsAPIAsync """Instance of the Test Results API for making asynchronous requests.""" - file_attachments: FileAttachmentsAPIAsync - """Instance of the File Attachments API for making asynchronous requests.""" - def count_non_none(*args: Any) -> int: """Count the number of non-none arguments.""" From 90afb7326d0e119a8e779e9de72bbcb88887e07e Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 10 Nov 2025 13:58:23 -0800 Subject: [PATCH 042/127] format --- python/lib/sift_client/resources/file_attachments.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 3ec481111..01f6987a8 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -136,4 +136,3 @@ async def get_download_url(self, *, file_attachment_id: str) -> str: return await self._low_level_client.get_remote_file_download_url( remote_file_id=file_attachment_id ) - From d9a46bd00045d81b31e9bca5b86b96ebc1bfd227 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 10 Nov 2025 14:16:56 -0800 Subject: [PATCH 043/127] clean up --- .../resources/sync_stubs/__init__.pyi | 97 +++++++++++++++++++ .../sift_types/_mixins/file_attachments.py | 2 +- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index d2f8a99e6..1bf713b1c 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -18,6 +18,7 @@ from sift_client.sift_types.calculated_channel import ( CalculatedChannelUpdate, ) from sift_client.sift_types.channel import Channel +from sift_client.sift_types.file_attachment import FileAttachment, RemoteFileUpdate from sift_client.sift_types.report import Report, ReportUpdate from sift_client.sift_types.rule import Rule, RuleCreate, RuleUpdate from sift_client.sift_types.run import Run, RunCreate, RunUpdate @@ -503,6 +504,102 @@ class ChannelsAPI: """ ... +class FileAttachmentsAPI: + """Sync counterpart to `FileAttachmentsAPIAsync`. + + High-level API for interacting with file attachments (remote files). + + This class provides a Pythonic interface for managing file attachments + on Sift entities like runs, assets, and test reports. + """ + + def __init__(self, sift_client: SiftClient): + """Initialize the FileAttachmentsAPI. + + Args: + sift_client: The Sift client to use. + """ + ... + + def _run(self, coro): ... + def get(self, *, file_attachment_id: str) -> FileAttachment: + """Get a file attachment by ID. + + Args: + file_attachment_id: The ID of the file attachment to retrieve. + + Returns: + The FileAttachment. + """ + ... + + def list_( + self, + *, + entity_type: str | None = None, + entity_id: str | None = None, + query_filter: str | None = None, + order_by: str | None = None, + limit: int | None = None, + page_size: int | None = None, + ) -> list[FileAttachment]: + """List file attachments with optional filtering. + + Args: + entity_type: Filter by entity type (e.g., 'ENTITY_TYPE_ASSET', 'ENTITY_TYPE_RUN'). + entity_id: Filter by entity ID. + query_filter: Optional CEL query filter. + order_by: The field to order by. + limit: Maximum number of results to return. + page_size: Number of results per page. + + Returns: + A list of FileAttachments. + """ + ... + + def update( + self, + *, + file_attachment: RemoteFileUpdate | dict, + ) -> FileAttachment: + """Update a file attachment. + + Args: + file_attachment: The RemoteFileUpdate with fields to update. + + Returns: + The updated FileAttachment. + """ + ... + + def delete(self, *, file_attachment_id: str) -> None: + """Delete a file attachment. + + Args: + file_attachment_id: The ID of the file attachment to delete. + """ + ... + + def batch_delete(self, *, file_attachment_ids: list[str]) -> None: + """Batch delete multiple file attachments. + + Args: + file_attachment_ids: List of file attachment IDs to delete (up to 1000). + """ + ... + + def get_download_url(self, *, file_attachment_id: str) -> str: + """Get a download URL for a file attachment. + + Args: + file_attachment_id: The ID of the file attachment. + + Returns: + The download URL for the file attachment. + """ + ... + class PingAPI: """Sync counterpart to `PingAPIAsync`. diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index d537a8ce0..8fbbfb979 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -64,7 +64,7 @@ def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]: Returns: A list of FileAttachments associated with this entity. """ - return self.client.file_attachments.list( + return self.client.file_attachments.list_( entity_type=self._get_entity_type_name(), # type: ignore[attr-defined] entity_id=self.id_, ) From 69dd11dcd70e22328db6c5030fe4d5bbf1cc519f Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 10 Nov 2025 14:32:26 -0800 Subject: [PATCH 044/127] test fixes --- python/lib/sift_client/_tests/conftest.py | 1 + .../_tests/sift_types/test_asset.py | 69 +++------------ .../_tests/sift_types/test_results.py | 70 +++------------ .../sift_client/_tests/sift_types/test_run.py | 85 +++++++------------ 4 files changed, 59 insertions(+), 166 deletions(-) diff --git a/python/lib/sift_client/_tests/conftest.py b/python/lib/sift_client/_tests/conftest.py index 08d418e83..117be4b9e 100644 --- a/python/lib/sift_client/_tests/conftest.py +++ b/python/lib/sift_client/_tests/conftest.py @@ -60,6 +60,7 @@ def mock_client(): client.rules = MagicMock() client.tags = MagicMock() client.test_results = MagicMock() + client.file_attachments = MagicMock() client.async_ = MagicMock(spec=AsyncAPIs) client.async_.ingestion = MagicMock() return client diff --git a/python/lib/sift_client/_tests/sift_types/test_asset.py b/python/lib/sift_client/_tests/sift_types/test_asset.py index a267a92a5..667760410 100644 --- a/python/lib/sift_client/_tests/sift_types/test_asset.py +++ b/python/lib/sift_client/_tests/sift_types/test_asset.py @@ -164,67 +164,24 @@ def test_update_calls_client_and_updates_self(self, mock_asset, mock_client): # Verify it returns self assert result is mock_asset - @pytest.mark.asyncio - async def test_remote_files_property_fetches_files(self, mock_asset, mock_client): - """Test that remote_files property fetches files from low-level client.""" - from unittest.mock import AsyncMock, patch - + def test_attachments_property_fetches_files(self, mock_asset, mock_client): + """Test that attachments property fetches files from client.file_attachments API.""" # Create mock remote files mock_remote_file = MagicMock() mock_remote_file.entity_id = mock_asset.id_ mock_remote_files = [mock_remote_file] - # Mock the low-level client - with patch( - "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" - ) as mock_low_level_client: - mock_low_level_client_instance = AsyncMock() - mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files - mock_low_level_client.return_value = mock_low_level_client_instance - - # Call remote_files method - result = await mock_asset.remote_files() - - # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(mock_client.grpc_client) - - # Verify list_all_remote_files was called with correct filter - mock_low_level_client_instance.list_all_remote_files.assert_called_once() - call_kwargs = mock_low_level_client_instance.list_all_remote_files.call_args.kwargs - assert "query_filter" in call_kwargs - assert mock_asset.id_ in call_kwargs["query_filter"] - assert "ENTITY_TYPE_ASSET" in call_kwargs["query_filter"] - - # Verify result - assert result == mock_remote_files - - @pytest.mark.asyncio - async def test_remote_file_fetches_single_file(self, mock_asset, mock_client): - """Test that remote_file fetches a single file by ID from low-level client.""" - from unittest.mock import AsyncMock, patch - - # Create mock remote file - file_id = "remote_file_123" - mock_remote_file = MagicMock() - mock_remote_file.id_ = file_id - mock_remote_file.entity_id = mock_asset.id_ - - # Mock the low-level client - with patch( - "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" - ) as mock_low_level_client: - mock_low_level_client_instance = AsyncMock() - mock_low_level_client_instance.get_remote_file.return_value = mock_remote_file - mock_low_level_client.return_value = mock_low_level_client_instance - - # Call remote_file method - result = await mock_asset.remote_file(file_id) + # Mock the file_attachments API + mock_client.file_attachments.list_.return_value = mock_remote_files - # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(mock_client.grpc_client) + # Access the attachments property (it's a property, not a method) + result = mock_asset.attachments - # Verify get_remote_file was called with correct file_id - mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) + # Verify file_attachments.list_ was called with correct parameters + mock_client.file_attachments.list_.assert_called_once_with( + entity_type="ENTITY_TYPE_ASSET", + entity_id=mock_asset.id_, + ) - # Verify result - assert result == mock_remote_file + # Verify result + assert result == mock_remote_files diff --git a/python/lib/sift_client/_tests/sift_types/test_results.py b/python/lib/sift_client/_tests/sift_types/test_results.py index 26ea70d44..1bee37df5 100644 --- a/python/lib/sift_client/_tests/sift_types/test_results.py +++ b/python/lib/sift_client/_tests/sift_types/test_results.py @@ -210,68 +210,24 @@ def test_step_measurements(self, mock_test_step, mock_test_measurement, mock_cli assert len(measurements) == 1 assert measurements[0] == mock_test_measurement - @pytest.mark.asyncio - async def test_remote_files_property_fetches_files(self, mock_test_report, mock_client): - """Test that remote_files property fetches files from low-level client.""" - from unittest.mock import AsyncMock, patch - + def test_attachments_property_fetches_files(self, mock_test_report, mock_client): + """Test that attachments property fetches files from client.file_attachments API.""" # Create mock remote files mock_remote_file = MagicMock() mock_remote_file.entity_id = mock_test_report.id_ mock_remote_files = [mock_remote_file] - # Mock the low-level client - with patch( - "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" - ) as mock_low_level_client: - mock_low_level_client_instance = AsyncMock() - mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files - mock_low_level_client.return_value = mock_low_level_client_instance - - # Call remote_files method - result = await mock_test_report.remote_files() - - # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(mock_client.grpc_client) - - # Verify list_all_remote_files was called with correct filter - mock_low_level_client_instance.list_all_remote_files.assert_called_once() - call_kwargs = mock_low_level_client_instance.list_all_remote_files.call_args.kwargs - assert "query_filter" in call_kwargs - # Verify the filter contains both the test_report id and entity_type - assert mock_test_report.id_ in call_kwargs["query_filter"] - assert "ENTITY_TYPE_TEST_REPORT" in call_kwargs["query_filter"] - - # Verify result - assert result == mock_remote_files - - @pytest.mark.asyncio - async def test_remote_file_fetches_single_file(self, mock_test_report, mock_client): - """Test that remote_file fetches a single file by ID from low-level client.""" - from unittest.mock import AsyncMock, patch + # Mock the file_attachments API + mock_client.file_attachments.list_.return_value = mock_remote_files - # Create mock remote file - file_id = "remote_file_123" - mock_remote_file = MagicMock() - mock_remote_file.id_ = file_id - mock_remote_file.entity_id = mock_test_report.id_ - - # Mock the low-level client - with patch( - "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" - ) as mock_low_level_client: - mock_low_level_client_instance = AsyncMock() - mock_low_level_client_instance.get_remote_file.return_value = mock_remote_file - mock_low_level_client.return_value = mock_low_level_client_instance + # Access the attachments property (it's a property, not a method) + result = mock_test_report.attachments - # Call remote_file method - result = await mock_test_report.remote_file(file_id) - - # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(mock_client.grpc_client) - - # Verify get_remote_file was called with correct file_id - mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) + # Verify file_attachments.list_ was called with correct parameters + mock_client.file_attachments.list_.assert_called_once_with( + entity_type="ENTITY_TYPE_TEST_REPORT", + entity_id=mock_test_report.id_, + ) - # Verify result - assert result == mock_remote_file + # Verify result + assert result == mock_remote_files diff --git a/python/lib/sift_client/_tests/sift_types/test_run.py b/python/lib/sift_client/_tests/sift_types/test_run.py index 5a1ba140f..34dd77b30 100644 --- a/python/lib/sift_client/_tests/sift_types/test_run.py +++ b/python/lib/sift_client/_tests/sift_types/test_run.py @@ -206,70 +206,49 @@ def test_update_calls_client_and_updates_self(self, mock_run, mock_client): # Verify it returns self assert result is mock_run - @pytest.mark.asyncio - async def test_remote_files_property_fetches_files(self, mock_run, mock_client): - """Test that remote_files property fetches files from low-level client.""" - from unittest.mock import AsyncMock, patch - + def test_remote_files_property_fetches_files(self, mock_run, mock_client): + """Test that attachments property fetches files from client.file_attachments API.""" # Create mock remote files mock_remote_file = MagicMock() mock_remote_file.entity_id = mock_run.id_ mock_remote_files = [mock_remote_file] - # Mock the low-level client - with patch( - "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" - ) as mock_low_level_client: - mock_low_level_client_instance = AsyncMock() - mock_low_level_client_instance.list_all_remote_files.return_value = mock_remote_files - mock_low_level_client.return_value = mock_low_level_client_instance - - # Call remote_files method - result = await mock_run.remote_files() - - # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(mock_client.grpc_client) - - # Verify list_all_remote_files was called with correct filter - mock_low_level_client_instance.list_all_remote_files.assert_called_once() - call_kwargs = mock_low_level_client_instance.list_all_remote_files.call_args.kwargs - assert "query_filter" in call_kwargs - assert mock_run.id_ in call_kwargs["query_filter"] - assert "ENTITY_TYPE_RUN" in call_kwargs["query_filter"] - - # Verify result - assert result == mock_remote_files - - @pytest.mark.asyncio - async def test_remote_file_fetches_single_file(self, mock_run, mock_client): - """Test that remote_file fetches a single file by ID from low-level client.""" - from unittest.mock import AsyncMock, patch - - # Create mock remote file - file_id = "remote_file_123" + # Mock the file_attachments API + mock_client.file_attachments.list_.return_value = mock_remote_files + + # Access the attachments property (it's a property, not a method) + result = mock_run.attachments + + # Verify file_attachments.list_ was called with correct parameters + mock_client.file_attachments.list_.assert_called_once_with( + entity_type="ENTITY_TYPE_RUN", + entity_id=mock_run.id_, + ) + + # Verify result + assert result == mock_remote_files + + def test_attachments_property_fetches_files(self, mock_run, mock_client): + """Test that attachments property fetches files from client.file_attachments API.""" + # Create mock remote files mock_remote_file = MagicMock() - mock_remote_file.id_ = file_id mock_remote_file.entity_id = mock_run.id_ + mock_remote_files = [mock_remote_file] - # Mock the low-level client - with patch( - "sift_client._internal.low_level_wrappers.RemoteFilesLowLevelClient" - ) as mock_low_level_client: - mock_low_level_client_instance = AsyncMock() - mock_low_level_client_instance.get_remote_file.return_value = mock_remote_file - mock_low_level_client.return_value = mock_low_level_client_instance - - # Call remote_file method - result = await mock_run.remote_file(file_id) + # Mock the file_attachments API + mock_client.file_attachments.list_.return_value = mock_remote_files - # Verify low-level client was instantiated with grpc_client - mock_low_level_client.assert_called_once_with(mock_client.grpc_client) + # Access the attachments property (it's a property, not a method) + result = mock_run.attachments - # Verify get_remote_file was called with correct file_id - mock_low_level_client_instance.get_remote_file.assert_called_once_with(file_id) + # Verify file_attachments.list_ was called with correct parameters + mock_client.file_attachments.list_.assert_called_once_with( + entity_type="ENTITY_TYPE_RUN", + entity_id=mock_run.id_, + ) - # Verify result - assert result == mock_remote_file + # Verify result + assert result == mock_remote_files def test_run_stop(self, mock_run, mock_client): """Test that stop() calls client.runs.stop and updates self.""" From cb9011a3036ffeb263b4f9109aee85d56d5c8b86 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 11 Nov 2025 14:25:26 -0800 Subject: [PATCH 045/127] update file attachments --- .../sift_client/resources/file_attachments.py | 69 +++++++++++++------ .../sift_client/sift_types/file_attachment.py | 63 ++++------------- 2 files changed, 62 insertions(+), 70 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 01f6987a8..6e5defdbf 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING -from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient from sift_client.resources._base import ResourceBase if TYPE_CHECKING: @@ -24,7 +23,6 @@ def __init__(self, sift_client: SiftClient): sift_client: The Sift client to use. """ super().__init__(sift_client) - self._low_level_client = RemoteFilesLowLevelClient(grpc_client=self.client.grpc_client) async def get(self, *, file_attachment_id: str) -> FileAttachment: """Get a file attachment by ID. @@ -35,10 +33,7 @@ async def get(self, *, file_attachment_id: str) -> FileAttachment: Returns: The FileAttachment. """ - file_attachment = await self._low_level_client.get_remote_file( - remote_file_id=file_attachment_id, - sift_client=self.client, - ) + file_attachment = await self.client.file_attachments.get(file_attachment_id=file_attachment_id) return self._apply_client_to_instance(file_attachment) async def list_( @@ -75,7 +70,7 @@ async def list_( combined_filter = " && ".join(filters) if filters else None - file_attachments = await self._low_level_client.list_all_remote_files( + file_attachments = await self.client.file_attachments.list_( query_filter=combined_filter, order_by=order_by, max_results=limit, @@ -108,23 +103,28 @@ async def update( ) return self._apply_client_to_instance(updated) - async def delete(self, *, file_attachment_id: str) -> None: - """Delete a file attachment. - - Args: - file_attachment_id: The ID of the file attachment to delete. - """ - await self._low_level_client.delete_remote_file(remote_file_id=file_attachment_id) - - async def batch_delete(self, *, file_attachment_ids: list[str]) -> None: + async def delete(self, *, file_attachments: list[FileAttachment | str] | FileAttachment | str) -> None: """Batch delete multiple file attachments. Args: - file_attachment_ids: List of file attachment IDs to delete (up to 1000). + file_attachments: List of FileAttachments or the IDs of the file attachments to delete (up to 1000). """ - await self._low_level_client.batch_delete_remote_files(remote_file_ids=file_attachment_ids) - - async def get_download_url(self, *, file_attachment_id: str) -> str: + file_attachment_ids = [] + if isinstance(file_attachments, FileAttachment): + file_attachment_ids.append(file_attachment.id_) + elif isinstance(file_attachments, str): + file_attachment_ids.append(file_attachment) + elif isinstance(file_attachments, list): + for file_attachment in file_attachments: + if isinstance(file_attachment, FileAttachment): + file_attachment_ids.append(file_attachment.id_) + elif isinstance(file_attachment, str): + file_attachment_ids.append(file_attachment) + else: + raise ValueError("file_attachments must be a list of FileAttachment or list of str") + await self.low_level_client.batch_delete_remote_files(file_attachment_ids=file_attachment_ids) + + async def get_download_url(self, *, file_attachment: FileAttachment | str) -> str: """Get a download URL for a file attachment. Args: @@ -133,6 +133,31 @@ async def get_download_url(self, *, file_attachment_id: str) -> str: Returns: The download URL for the file attachment. """ - return await self._low_level_client.get_remote_file_download_url( - remote_file_id=file_attachment_id + if isinstance(file_attachment, FileAttachment): + file_attachment_id = file_attachment.id_ + elif isinstance(file_attachment, str): + file_attachment_id = file_attachment + else: + raise ValueError("file_attachment must be a FileAttachment or a string") + return await self.low_level_client.get_remote_file_download_url( + file_attachment_id=file_attachment_id ) + + async def download(self, *, file_attachment: FileAttachment | str, output_path: str | Path) -> None: + """Download a file attachment to a local path. + + Args: + file_attachment: The FileAttachment or the ID of the file attachment to download. + output_path: The path to download the file attachment to. + """ + if isinstance(file_attachment, FileAttachment): + file_attachment_id = file_attachment.id_ + elif isinstance(file_attachment, str): + file_attachment_id = file_attachment + else: + raise ValueError("file_attachment must be a FileAttachment or a string") + download_url = await self.get_download_url(file_attachment=file_attachment_id) + response = requests.get(download_url) + response.raise_for_status() + with open(output_path, "wb") as f: + f.write(response.content) \ No newline at end of file diff --git a/python/lib/sift_client/sift_types/file_attachment.py b/python/lib/sift_client/sift_types/file_attachment.py index 81f127423..63f49974a 100644 --- a/python/lib/sift_client/sift_types/file_attachment.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -57,7 +57,7 @@ def from_proto_value(proto_value: int) -> RemoteFileEntityType: class FileAttachment(BaseType[RemoteFileProto, "FileAttachment"]): - """Model of the Sift RemoteFile.""" + """Model of the Sift FileAttachment.""" organization_id: str entity_id: str @@ -97,7 +97,7 @@ def _from_proto( @property def entity(self) -> Run | Asset | TestReport: - """Get the entity that this remote file is attached to.""" + """Get the entity that this file attachment is attached to.""" if self.entity_type == RemoteFileEntityType.RUNS: return self.client.runs.get(run_id=self.entity_id) elif self.entity_type == RemoteFileEntityType.ASSETS: @@ -115,70 +115,37 @@ def entity(self) -> Run | Asset | TestReport: raise Exception(f"Unknown remote file entity type: {self.entity_type}") def delete(self) -> None: - """Delete the remote file.""" + """Delete the file attachment.""" if self.id_ is None: raise ValueError("Remote file ID is not set") - from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - - remote_files_client = RemoteFilesLowLevelClient(self.client.grpc_client) - loop = self.client.get_asyncio_loop() - asyncio.run_coroutine_threadsafe( - remote_files_client.delete_remote_file(remote_file_id=self.id_), loop - ).result() + self.client.file_attachments.delete(file_attachment=self.id_) def update(self, update: RemoteFileUpdate | dict) -> FileAttachment: - """Update the remote file.""" - from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - + """Update the file attachment.""" if isinstance(update, dict): update = RemoteFileUpdate.model_validate(update) if self.id_ is None: raise ValueError("Remote file ID is not set") update.resource_id = self.id_ - remote_file_client = RemoteFilesLowLevelClient(self.client.grpc_client) - loop = self.client.get_asyncio_loop() - updated_remote_file = cast( - "FileAttachment", - asyncio.run_coroutine_threadsafe( - remote_file_client.update_remote_file(update=update, sift_client=self.client), loop - ).result(), - ) - return updated_remote_file + updated_file_attachment = self.client.file_attachments.update(file_attachment=update) + self._update(updated_file_attachment) + return self def download_url(self) -> str: - """Get the download URL for the remote file.""" + """Get the download URL for the file attachment.""" if self.id_ is None: - raise ValueError("Remote file ID is not set") - from sift_client._internal.low_level_wrappers import RemoteFilesLowLevelClient - - remote_files_client = RemoteFilesLowLevelClient(self.client.grpc_client) - loop = self.client.get_asyncio_loop() - return asyncio.run_coroutine_threadsafe( - remote_files_client.get_remote_file_download_url(remote_file_id=self.id_), loop - ).result() + raise ValueError("file attachment ID is not set") + return self.client.file_attachments.get_download_url(file_attachment=self) def download(self, output_path: str | Path) -> None: - """Download the remote file to a local path.""" + """Download the file attachment to a local path.""" # Get the download URL - download_url = self.download_url() - - # Convert output_path to Path object for easier handling - output_path = Path(output_path) - - # Ensure the parent directory exists - output_path.parent.mkdir(parents=True, exist_ok=True) - - # Download the file content - response = requests.get(download_url) - response.raise_for_status() - - # Write the content to the output file - output_path.write_bytes(response.content) + self.client.file_attachments.download(file_attachment=self, output_path=output_path) -class RemoteFileUpdate(ModelUpdate[RemoteFileProto]): - """Model of the RemoteFile fields that can be updated.""" +class FileAttachmentUpdate(ModelUpdate[RemoteFileProto]): + """Model of the FileAttachment fields that can be updated.""" description: str | None = None From e0c8075c68ec0143b882f35dc5a6d8c3e5ee3c80 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 11 Nov 2025 16:34:33 -0800 Subject: [PATCH 046/127] clean up --- .../low_level_wrappers/remote_files.py | 2 +- .../sift_client/resources/file_attachments.py | 47 +++++++++---------- .../sift_client/sift_types/file_attachment.py | 14 +++--- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 5d062aa97..aa801cc40 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.sift_types.file_attachment import FileAttachment, RemoteFileUpdate + from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate class RemoteFilesLowLevelClient(LowLevelClientBase, WithGrpcClient): diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 6e5defdbf..6923fcbb0 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -1,12 +1,14 @@ from __future__ import annotations +from pathlib import Path from typing import TYPE_CHECKING +from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient from sift_client.resources._base import ResourceBase +from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.sift_types.file_attachment import FileAttachment, RemoteFileUpdate class FileAttachmentsAPIAsync(ResourceBase): @@ -23,6 +25,7 @@ def __init__(self, sift_client: SiftClient): sift_client: The Sift client to use. """ super().__init__(sift_client) + self._low_level_client = RemoteFilesLowLevelClient(grpc_client=self.client.grpc_client) async def get(self, *, file_attachment_id: str) -> FileAttachment: """Get a file attachment by ID. @@ -33,7 +36,10 @@ async def get(self, *, file_attachment_id: str) -> FileAttachment: Returns: The FileAttachment. """ - file_attachment = await self.client.file_attachments.get(file_attachment_id=file_attachment_id) + file_attachment = await self._low_level_client.get_remote_file( + remote_file_id=file_attachment_id, + sift_client=self.client, + ) return self._apply_client_to_instance(file_attachment) async def list_( @@ -70,7 +76,7 @@ async def list_( combined_filter = " && ".join(filters) if filters else None - file_attachments = await self.client.file_attachments.list_( + file_attachments = await self._low_level_client.list_all_remote_files( query_filter=combined_filter, order_by=order_by, max_results=limit, @@ -82,20 +88,18 @@ async def list_( async def update( self, *, - file_attachment: RemoteFileUpdate | dict, + file_attachment: FileAttachmentUpdate | dict, ) -> FileAttachment: """Update a file attachment. Args: - file_attachment: The RemoteFileUpdate with fields to update. + file_attachment: The FileAttachmentUpdate with fields to update. Returns: The updated FileAttachment. """ - from sift_client.sift_types.file_attachment import RemoteFileUpdate - if isinstance(file_attachment, dict): - file_attachment = RemoteFileUpdate.model_validate(file_attachment) + file_attachment = FileAttachmentUpdate.model_validate(file_attachment) updated = await self._low_level_client.update_remote_file( update=file_attachment, @@ -111,9 +115,9 @@ async def delete(self, *, file_attachments: list[FileAttachment | str] | FileAtt """ file_attachment_ids = [] if isinstance(file_attachments, FileAttachment): - file_attachment_ids.append(file_attachment.id_) + file_attachment_ids.append(file_attachments.id_) elif isinstance(file_attachments, str): - file_attachment_ids.append(file_attachment) + file_attachment_ids.append(file_attachments) elif isinstance(file_attachments, list): for file_attachment in file_attachments: if isinstance(file_attachment, FileAttachment): @@ -122,13 +126,13 @@ async def delete(self, *, file_attachments: list[FileAttachment | str] | FileAtt file_attachment_ids.append(file_attachment) else: raise ValueError("file_attachments must be a list of FileAttachment or list of str") - await self.low_level_client.batch_delete_remote_files(file_attachment_ids=file_attachment_ids) + await self._low_level_client.batch_delete_remote_files(remote_file_ids=file_attachment_ids) async def get_download_url(self, *, file_attachment: FileAttachment | str) -> str: """Get a download URL for a file attachment. Args: - file_attachment_id: The ID of the file attachment. + file_attachment: The FileAttachment or the ID of the file attachment. Returns: The download URL for the file attachment. @@ -139,8 +143,8 @@ async def get_download_url(self, *, file_attachment: FileAttachment | str) -> st file_attachment_id = file_attachment else: raise ValueError("file_attachment must be a FileAttachment or a string") - return await self.low_level_client.get_remote_file_download_url( - file_attachment_id=file_attachment_id + return await self._low_level_client.get_remote_file_download_url( + remote_file_id=file_attachment_id ) async def download(self, *, file_attachment: FileAttachment | str, output_path: str | Path) -> None: @@ -150,14 +154,7 @@ async def download(self, *, file_attachment: FileAttachment | str, output_path: file_attachment: The FileAttachment or the ID of the file attachment to download. output_path: The path to download the file attachment to. """ - if isinstance(file_attachment, FileAttachment): - file_attachment_id = file_attachment.id_ - elif isinstance(file_attachment, str): - file_attachment_id = file_attachment - else: - raise ValueError("file_attachment must be a FileAttachment or a string") - download_url = await self.get_download_url(file_attachment=file_attachment_id) - response = requests.get(download_url) - response.raise_for_status() - with open(output_path, "wb") as f: - f.write(response.content) \ No newline at end of file + from sift_py.file_attachment._internal.download import download_remote_file + + download_url = await self.get_download_url(file_attachment=file_attachment) + download_remote_file(download_url, Path(output_path)) diff --git a/python/lib/sift_client/sift_types/file_attachment.py b/python/lib/sift_client/sift_types/file_attachment.py index 63f49974a..9e0598fd1 100644 --- a/python/lib/sift_client/sift_types/file_attachment.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -1,18 +1,17 @@ from __future__ import annotations -import asyncio from datetime import datetime, timezone from enum import Enum -from pathlib import Path -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING -import requests from sift.remote_files.v1.remote_files_pb2 import EntityType from sift.remote_files.v1.remote_files_pb2 import RemoteFile as RemoteFileProto from sift_client.sift_types._base import BaseType, ModelUpdate if TYPE_CHECKING: + from pathlib import Path + from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset from sift_client.sift_types.run import Run @@ -120,10 +119,10 @@ def delete(self) -> None: raise ValueError("Remote file ID is not set") self.client.file_attachments.delete(file_attachment=self.id_) - def update(self, update: RemoteFileUpdate | dict) -> FileAttachment: + def update(self, update: FileAttachmentUpdate | dict) -> FileAttachment: """Update the file attachment.""" if isinstance(update, dict): - update = RemoteFileUpdate.model_validate(update) + update = FileAttachmentUpdate.model_validate(update) if self.id_ is None: raise ValueError("Remote file ID is not set") update.resource_id = self.id_ @@ -140,8 +139,7 @@ def download_url(self) -> str: def download(self, output_path: str | Path) -> None: """Download the file attachment to a local path.""" - # Get the download URL - self.client.file_attachments.download(file_attachment=self, output_path=output_path) + self.client.file_attachments.download(file_attachment=self, output_path=output_path) class FileAttachmentUpdate(ModelUpdate[RemoteFileProto]): From bfb3bf3c341bd34495e86c78e4651ccea9fbd841 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 11 Nov 2025 16:39:31 -0800 Subject: [PATCH 047/127] cleanup --- .../_internal/low_level_wrappers/remote_files.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index aa801cc40..3412213f6 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -18,11 +18,11 @@ from sift_client._internal.low_level_wrappers.base import ( LowLevelClientBase, ) +from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate from sift_client.transport import GrpcClient, WithGrpcClient if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate class RemoteFilesLowLevelClient(LowLevelClientBase, WithGrpcClient): @@ -126,19 +126,17 @@ async def list_remote_files( ], response.next_page_token async def update_remote_file( - self, update: RemoteFileUpdate, sift_client: SiftClient | None = None + self, update: FileAttachmentUpdate, sift_client: SiftClient | None = None ) -> FileAttachment: """Update a remote file. Args: - update: The RemoteFileUpdate containing the fields to update. + update: The FileAttachmentUpdate containing the fields to update. sift_client: The SiftClient to attach to the returned RemoteFile. Returns: The updated RemoteFile. """ - from sift_client.sift_types.file_attachment import FileAttachment - grpc_remote_file, update_mask = update.to_proto_with_mask() request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask) response = await self._grpc_client.get_stub(RemoteFileServiceStub).UpdateRemoteFile(request) From 0c13067dda8f53771721cd1ad8082bd6b963c406 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 11 Nov 2025 16:45:34 -0800 Subject: [PATCH 048/127] cleanup --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index debb90c72..6e2607109 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -9,6 +9,7 @@ if TYPE_CHECKING: from datetime import datetime, timedelta from pathlib import Path from typing import TYPE_CHECKING, Any + import pandas as pd import pyarrow as pa From 66e9a8b9682b6d3c6fd71e8dc31c728d31cbc6b9 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 11 Nov 2025 16:47:44 -0800 Subject: [PATCH 049/127] format --- .../sift_client/resources/file_attachments.py | 12 +- .../sift_stream_bindings.pyi | 188 ++++++++++++------ .../sift_stream_bindings/tests/test_stream.py | 3 +- .../sift_stream_bindings/tests/test_value.py | 2 +- 4 files changed, 135 insertions(+), 70 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 6923fcbb0..4fe9959ba 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -107,7 +107,9 @@ async def update( ) return self._apply_client_to_instance(updated) - async def delete(self, *, file_attachments: list[FileAttachment | str] | FileAttachment | str) -> None: + async def delete( + self, *, file_attachments: list[FileAttachment | str] | FileAttachment | str + ) -> None: """Batch delete multiple file attachments. Args: @@ -125,7 +127,9 @@ async def delete(self, *, file_attachments: list[FileAttachment | str] | FileAtt elif isinstance(file_attachment, str): file_attachment_ids.append(file_attachment) else: - raise ValueError("file_attachments must be a list of FileAttachment or list of str") + raise ValueError( + "file_attachments must be a list of FileAttachment or list of str" + ) await self._low_level_client.batch_delete_remote_files(remote_file_ids=file_attachment_ids) async def get_download_url(self, *, file_attachment: FileAttachment | str) -> str: @@ -147,7 +151,9 @@ async def get_download_url(self, *, file_attachment: FileAttachment | str) -> st remote_file_id=file_attachment_id ) - async def download(self, *, file_attachment: FileAttachment | str, output_path: str | Path) -> None: + async def download( + self, *, file_attachment: FileAttachment | str, output_path: str | Path + ) -> None: """Download a file attachment to a local path. Args: diff --git a/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi b/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi index 5632fc19e..c0ae85028 100644 --- a/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi +++ b/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi @@ -53,7 +53,9 @@ class ChannelBitFieldElementPy: name: builtins.str index: builtins.int bit_count: builtins.int - def __new__(cls, name:builtins.str, index:builtins.int, bit_count:builtins.int) -> ChannelBitFieldElementPy: ... + def __new__( + cls, name: builtins.str, index: builtins.int, bit_count: builtins.int + ) -> ChannelBitFieldElementPy: ... @typing.final class ChannelConfigPy: @@ -63,48 +65,56 @@ class ChannelConfigPy: data_type: ChannelDataTypePy enum_types: builtins.list[ChannelEnumTypePy] bit_field_elements: builtins.list[ChannelBitFieldElementPy] - def __new__(cls, name:builtins.str, unit:builtins.str, description:builtins.str, data_type:ChannelDataTypePy, enum_types:typing.Sequence[ChannelEnumTypePy], bit_field_elements:typing.Sequence[ChannelBitFieldElementPy]) -> ChannelConfigPy: ... + def __new__( + cls, + name: builtins.str, + unit: builtins.str, + description: builtins.str, + data_type: ChannelDataTypePy, + enum_types: typing.Sequence[ChannelEnumTypePy], + bit_field_elements: typing.Sequence[ChannelBitFieldElementPy], + ) -> ChannelConfigPy: ... @typing.final class ChannelEnumPy: - def __new__(cls, val:builtins.int) -> ChannelEnumPy: ... + def __new__(cls, val: builtins.int) -> ChannelEnumPy: ... @typing.final class ChannelEnumTypePy: name: builtins.str key: builtins.int - def __new__(cls, name:builtins.str, key:builtins.int) -> ChannelEnumTypePy: ... + def __new__(cls, name: builtins.str, key: builtins.int) -> ChannelEnumTypePy: ... @typing.final class ChannelValuePy: name: builtins.str value: ValuePy - def __new__(cls, name:builtins.str, value:ValuePy) -> ChannelValuePy: ... + def __new__(cls, name: builtins.str, value: ValuePy) -> ChannelValuePy: ... @typing.final class ChannelValueTypePy: @staticmethod - def bool(value:builtins.bool) -> ChannelValueTypePy: ... + def bool(value: builtins.bool) -> ChannelValueTypePy: ... @staticmethod - def string(value:builtins.str) -> ChannelValueTypePy: ... + def string(value: builtins.str) -> ChannelValueTypePy: ... @staticmethod - def float(value:builtins.float) -> ChannelValueTypePy: ... + def float(value: builtins.float) -> ChannelValueTypePy: ... @staticmethod - def double(value:builtins.float) -> ChannelValueTypePy: ... + def double(value: builtins.float) -> ChannelValueTypePy: ... @staticmethod - def int32(value:builtins.int) -> ChannelValueTypePy: ... + def int32(value: builtins.int) -> ChannelValueTypePy: ... @staticmethod - def uint32(value:builtins.int) -> ChannelValueTypePy: ... + def uint32(value: builtins.int) -> ChannelValueTypePy: ... @staticmethod - def int64(value:builtins.int) -> ChannelValueTypePy: ... + def int64(value: builtins.int) -> ChannelValueTypePy: ... @staticmethod - def uint64(value:builtins.int) -> ChannelValueTypePy: ... + def uint64(value: builtins.int) -> ChannelValueTypePy: ... @staticmethod - def enum_value(value:builtins.int) -> ChannelValueTypePy: ... + def enum_value(value: builtins.int) -> ChannelValueTypePy: ... @staticmethod - def bitfield(value:typing.Sequence[builtins.int]) -> ChannelValueTypePy: ... + def bitfield(value: typing.Sequence[builtins.int]) -> ChannelValueTypePy: ... @staticmethod - def bytes(value:typing.Sequence[builtins.int]) -> ChannelValueTypePy: ... + def bytes(value: typing.Sequence[builtins.int]) -> ChannelValueTypePy: ... @staticmethod def empty() -> ChannelValueTypePy: ... @@ -126,7 +136,13 @@ class DiskBackupPolicyPy: max_backup_file_size: builtins.int rolling_file_policy: RollingFilePolicyPy retain_backups: builtins.bool - def __new__(cls, backups_dir:typing.Optional[builtins.str], max_backup_file_size:builtins.int, rolling_file_policy:RollingFilePolicyPy, retain_backups:builtins.bool) -> DiskBackupPolicyPy: ... + def __new__( + cls, + backups_dir: typing.Optional[builtins.str], + max_backup_file_size: builtins.int, + rolling_file_policy: RollingFilePolicyPy, + retain_backups: builtins.bool, + ) -> DiskBackupPolicyPy: ... @staticmethod def default() -> DiskBackupPolicyPy: ... @@ -134,41 +150,50 @@ class DiskBackupPolicyPy: class DurationPy: secs: builtins.int nanos: builtins.int - def __new__(cls, secs:builtins.int, nanos:builtins.int) -> DurationPy: ... + def __new__(cls, secs: builtins.int, nanos: builtins.int) -> DurationPy: ... @typing.final class FlowConfigPy: name: builtins.str channels: builtins.list[ChannelConfigPy] - def __new__(cls, name:builtins.str, channels:typing.Sequence[ChannelConfigPy]) -> FlowConfigPy: ... + def __new__( + cls, name: builtins.str, channels: typing.Sequence[ChannelConfigPy] + ) -> FlowConfigPy: ... @typing.final class FlowPy: - def __new__(cls, flow_name:builtins.str, timestamp:TimeValuePy, values:typing.Sequence[ChannelValuePy]) -> FlowPy: ... + def __new__( + cls, + flow_name: builtins.str, + timestamp: TimeValuePy, + values: typing.Sequence[ChannelValuePy], + ) -> FlowPy: ... @typing.final class IngestWithConfigDataChannelValuePy: ty: ChannelValueTypePy @staticmethod - def bool(value:builtins.bool) -> IngestWithConfigDataChannelValuePy: ... + def bool(value: builtins.bool) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def string(value:builtins.str) -> IngestWithConfigDataChannelValuePy: ... + def string(value: builtins.str) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def float(value:builtins.float) -> IngestWithConfigDataChannelValuePy: ... + def float(value: builtins.float) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def double(value:builtins.float) -> IngestWithConfigDataChannelValuePy: ... + def double(value: builtins.float) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def int32(value:builtins.int) -> IngestWithConfigDataChannelValuePy: ... + def int32(value: builtins.int) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def uint32(value:builtins.int) -> IngestWithConfigDataChannelValuePy: ... + def uint32(value: builtins.int) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def int64(value:builtins.int) -> IngestWithConfigDataChannelValuePy: ... + def int64(value: builtins.int) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def uint64(value:builtins.int) -> IngestWithConfigDataChannelValuePy: ... + def uint64(value: builtins.int) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def enum_value(value:builtins.int) -> IngestWithConfigDataChannelValuePy: ... + def enum_value(value: builtins.int) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def bitfield(value:typing.Sequence[builtins.int]) -> IngestWithConfigDataChannelValuePy: ... + def bitfield( + value: typing.Sequence[builtins.int], + ) -> IngestWithConfigDataChannelValuePy: ... @staticmethod def empty() -> IngestWithConfigDataChannelValuePy: ... @@ -181,27 +206,43 @@ class IngestWithConfigDataStreamRequestPy: run_id: builtins.str end_stream_on_validation_error: builtins.bool organization_id: builtins.str - def __new__(cls, ingestion_config_id:builtins.str, flow:builtins.str, timestamp:typing.Optional[TimeValuePy], channel_values:typing.Sequence[IngestWithConfigDataChannelValuePy], run_id:builtins.str, end_stream_on_validation_error:builtins.bool, organization_id:builtins.str) -> IngestWithConfigDataStreamRequestPy: ... + def __new__( + cls, + ingestion_config_id: builtins.str, + flow: builtins.str, + timestamp: typing.Optional[TimeValuePy], + channel_values: typing.Sequence[IngestWithConfigDataChannelValuePy], + run_id: builtins.str, + end_stream_on_validation_error: builtins.bool, + organization_id: builtins.str, + ) -> IngestWithConfigDataStreamRequestPy: ... @typing.final class IngestionConfigFormPy: asset_name: builtins.str flows: builtins.list[FlowConfigPy] client_key: builtins.str - def __new__(cls, asset_name:builtins.str, client_key:builtins.str, flows:typing.Sequence[FlowConfigPy]) -> IngestionConfigFormPy: ... + def __new__( + cls, + asset_name: builtins.str, + client_key: builtins.str, + flows: typing.Sequence[FlowConfigPy], + ) -> IngestionConfigFormPy: ... @typing.final class MetadataPy: key: builtins.str value: MetadataValuePy - def __new__(cls, key:builtins.str, value:MetadataValuePy) -> MetadataPy: ... + def __new__(cls, key: builtins.str, value: MetadataValuePy) -> MetadataPy: ... @typing.final class RecoveryStrategyPy: @staticmethod - def retry_only(retry_policy:RetryPolicyPy) -> RecoveryStrategyPy: ... + def retry_only(retry_policy: RetryPolicyPy) -> RecoveryStrategyPy: ... @staticmethod - def retry_with_backups(retry_policy:RetryPolicyPy, disk_backup_policy:DiskBackupPolicyPy) -> RecoveryStrategyPy: ... + def retry_with_backups( + retry_policy: RetryPolicyPy, disk_backup_policy: DiskBackupPolicyPy + ) -> RecoveryStrategyPy: ... @staticmethod def default() -> RecoveryStrategyPy: ... @@ -211,13 +252,21 @@ class RetryPolicyPy: initial_backoff: DurationPy max_backoff: DurationPy backoff_multiplier: builtins.int - def __new__(cls, max_attempts:builtins.int, initial_backoff:DurationPy, max_backoff:DurationPy, backoff_multiplier:builtins.int) -> RetryPolicyPy: ... + def __new__( + cls, + max_attempts: builtins.int, + initial_backoff: DurationPy, + max_backoff: DurationPy, + backoff_multiplier: builtins.int, + ) -> RetryPolicyPy: ... @staticmethod def default() -> RetryPolicyPy: ... @typing.final class RollingFilePolicyPy: - def __new__(cls, max_file_count:typing.Optional[builtins.int]) -> RollingFilePolicyPy: ... + def __new__( + cls, max_file_count: typing.Optional[builtins.int] + ) -> RollingFilePolicyPy: ... @staticmethod def default() -> RollingFilePolicyPy: ... @@ -228,14 +277,21 @@ class RunFormPy: description: typing.Optional[builtins.str] tags: typing.Optional[builtins.list[builtins.str]] metadata: typing.Optional[builtins.list[MetadataPy]] - def __new__(cls, name:builtins.str, client_key:builtins.str, description:typing.Optional[builtins.str], tags:typing.Optional[typing.Sequence[builtins.str]], metadata:typing.Optional[typing.Sequence[MetadataPy]]) -> RunFormPy: ... + def __new__( + cls, + name: builtins.str, + client_key: builtins.str, + description: typing.Optional[builtins.str], + tags: typing.Optional[typing.Sequence[builtins.str]], + metadata: typing.Optional[typing.Sequence[MetadataPy]], + ) -> RunFormPy: ... @typing.final class RunSelectorPy: @staticmethod - def by_id(run_id:builtins.str) -> RunSelectorPy: ... + def by_id(run_id: builtins.str) -> RunSelectorPy: ... @staticmethod - def by_form(form:RunFormPy) -> RunSelectorPy: ... + def by_form(form: RunFormPy) -> RunSelectorPy: ... @typing.final class SiftStreamBuilderPy: @@ -249,7 +305,9 @@ class SiftStreamBuilderPy: run_id: typing.Optional[builtins.str] asset_tags: typing.Optional[builtins.list[builtins.str]] metadata: typing.Optional[builtins.list[MetadataPy]] - def __new__(cls, uri:builtins.str, apikey:builtins.str) -> SiftStreamBuilderPy: ... + def __new__( + cls, uri: builtins.str, apikey: builtins.str + ) -> SiftStreamBuilderPy: ... def build(self) -> typing.Any: ... @typing.final @@ -269,11 +327,15 @@ class SiftStreamMetricsSnapshotPy: @typing.final class SiftStreamPy: - def send(self, flow:FlowPy) -> typing.Any: ... - def send_requests(self, requests:typing.Sequence[IngestWithConfigDataStreamRequestPy]) -> typing.Any: ... + def send(self, flow: FlowPy) -> typing.Any: ... + def send_requests( + self, requests: typing.Sequence[IngestWithConfigDataStreamRequestPy] + ) -> typing.Any: ... def get_metrics_snapshot(self) -> SiftStreamMetricsSnapshotPy: ... - def add_new_flows(self, flow_configs:typing.Sequence[FlowConfigPy]) -> typing.Any: ... - def attach_run(self, run_selector:RunSelectorPy) -> typing.Any: ... + def add_new_flows( + self, flow_configs: typing.Sequence[FlowConfigPy] + ) -> typing.Any: ... + def attach_run(self, run_selector: RunSelectorPy) -> typing.Any: ... def detach_run(self) -> None: ... def run(self) -> typing.Optional[builtins.str]: ... def finish(self) -> typing.Any: ... @@ -282,38 +344,38 @@ class SiftStreamPy: class TimeValuePy: def __new__(cls) -> TimeValuePy: ... @staticmethod - def from_timestamp(secs:builtins.int, nsecs:builtins.int) -> TimeValuePy: ... + def from_timestamp(secs: builtins.int, nsecs: builtins.int) -> TimeValuePy: ... @staticmethod - def from_timestamp_millis(millis:builtins.int) -> TimeValuePy: ... + def from_timestamp_millis(millis: builtins.int) -> TimeValuePy: ... @staticmethod - def from_timestamp_micros(micros:builtins.int) -> TimeValuePy: ... + def from_timestamp_micros(micros: builtins.int) -> TimeValuePy: ... @staticmethod - def from_timestamp_nanos(nanos:builtins.int) -> TimeValuePy: ... + def from_timestamp_nanos(nanos: builtins.int) -> TimeValuePy: ... @staticmethod - def from_rfc3339(val:builtins.str) -> TimeValuePy: ... + def from_rfc3339(val: builtins.str) -> TimeValuePy: ... @typing.final class ValuePy: @staticmethod - def Bool(value:builtins.bool) -> ValuePy: ... + def Bool(value: builtins.bool) -> ValuePy: ... @staticmethod - def String(value:builtins.str) -> ValuePy: ... + def String(value: builtins.str) -> ValuePy: ... @staticmethod - def Float(value:builtins.float) -> ValuePy: ... + def Float(value: builtins.float) -> ValuePy: ... @staticmethod - def Double(value:builtins.float) -> ValuePy: ... + def Double(value: builtins.float) -> ValuePy: ... @staticmethod - def Int32(value:builtins.int) -> ValuePy: ... + def Int32(value: builtins.int) -> ValuePy: ... @staticmethod - def Int64(value:builtins.int) -> ValuePy: ... + def Int64(value: builtins.int) -> ValuePy: ... @staticmethod - def Uint32(value:builtins.int) -> ValuePy: ... + def Uint32(value: builtins.int) -> ValuePy: ... @staticmethod - def Uint64(value:builtins.int) -> ValuePy: ... + def Uint64(value: builtins.int) -> ValuePy: ... @staticmethod - def Enum(value:builtins.int) -> ValuePy: ... + def Enum(value: builtins.int) -> ValuePy: ... @staticmethod - def BitField(value:typing.Sequence[builtins.int]) -> ValuePy: ... + def BitField(value: typing.Sequence[builtins.int]) -> ValuePy: ... def is_bool(self) -> builtins.bool: ... def is_string(self) -> builtins.bool: ... def is_float(self) -> builtins.bool: ... @@ -356,12 +418,8 @@ class MetadataValuePy(Enum): Number = ... Boolean = ... - def __new__(cls, obj:typing.Any) -> MetadataValuePy: ... - + def __new__(cls, obj: typing.Any) -> MetadataValuePy: ... def __str__(self) -> builtins.str: ... - def is_string(self) -> builtins.bool: ... - def is_number(self) -> builtins.bool: ... - def is_boolean(self) -> builtins.bool: ... diff --git a/rust/crates/sift_stream_bindings/tests/test_stream.py b/rust/crates/sift_stream_bindings/tests/test_stream.py index f3ea81c51..cd83bedf9 100644 --- a/rust/crates/sift_stream_bindings/tests/test_stream.py +++ b/rust/crates/sift_stream_bindings/tests/test_stream.py @@ -11,7 +11,7 @@ SiftStreamBuilderPy, TimeValuePy, ValuePy, - IngestWithConfigDataStreamRequestPy + IngestWithConfigDataStreamRequestPy, ) @@ -27,6 +27,7 @@ def test_create_empty_flow(self): def test_create_flow_with_multiple_values(self): """Test creating a flow with multiple channel values.""" from sift_stream_bindings import ChannelValuePy + timestamp = TimeValuePy.from_timestamp(int(time.time()), 0) values = [ ChannelValuePy("temperature", ValuePy.Float(23.5)), diff --git a/rust/crates/sift_stream_bindings/tests/test_value.py b/rust/crates/sift_stream_bindings/tests/test_value.py index 4a670401d..ccddeaa6f 100644 --- a/rust/crates/sift_stream_bindings/tests/test_value.py +++ b/rust/crates/sift_stream_bindings/tests/test_value.py @@ -77,7 +77,7 @@ def test_create_bit_field_value(self): val = ValuePy.BitField([0x0A]) assert val.is_bitfield() - assert val.as_bitfield() == b'\x0A' + assert val.as_bitfield() == b"\x0a" def test_type_error_on_wrong_accessor(self): """Test that accessing with wrong type raises error.""" From 5fc3d344cbd086dc76cfacf7cbb67dce6c5b3113 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 11 Nov 2025 16:59:16 -0800 Subject: [PATCH 050/127] clean up --- .../sift_client/resources/file_attachments.py | 24 ++++++++++++++----- .../sift_client/sift_types/file_attachment.py | 2 +- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 4fe9959ba..82cd94f7a 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -115,15 +115,21 @@ async def delete( Args: file_attachments: List of FileAttachments or the IDs of the file attachments to delete (up to 1000). """ - file_attachment_ids = [] + file_attachment_ids: list[str] = [] if isinstance(file_attachments, FileAttachment): - file_attachment_ids.append(file_attachments.id_) + if file_attachments.id_ is not None: + file_attachment_ids.append(file_attachments.id_) + else: + raise ValueError("FileAttachment ID is not set") elif isinstance(file_attachments, str): file_attachment_ids.append(file_attachments) elif isinstance(file_attachments, list): for file_attachment in file_attachments: if isinstance(file_attachment, FileAttachment): - file_attachment_ids.append(file_attachment.id_) + if file_attachment.id_ is not None: + file_attachment_ids.append(file_attachment.id_) + else: + raise ValueError("FileAttachment ID is not set") elif isinstance(file_attachment, str): file_attachment_ids.append(file_attachment) else: @@ -141,14 +147,20 @@ async def get_download_url(self, *, file_attachment: FileAttachment | str) -> st Returns: The download URL for the file attachment. """ + id_: str = "" if isinstance(file_attachment, FileAttachment): - file_attachment_id = file_attachment.id_ + if file_attachment.id_ is not None: + id_ = file_attachment.id_ + else: + raise ValueError("FileAttachment ID is not set") elif isinstance(file_attachment, str): - file_attachment_id = file_attachment + id_ = file_attachment else: raise ValueError("file_attachment must be a FileAttachment or a string") + if id_ == "": + raise ValueError("FileAttachment ID is not set") return await self._low_level_client.get_remote_file_download_url( - remote_file_id=file_attachment_id + remote_file_id=id_ ) async def download( diff --git a/python/lib/sift_client/sift_types/file_attachment.py b/python/lib/sift_client/sift_types/file_attachment.py index 9e0598fd1..2281ef769 100644 --- a/python/lib/sift_client/sift_types/file_attachment.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -117,7 +117,7 @@ def delete(self) -> None: """Delete the file attachment.""" if self.id_ is None: raise ValueError("Remote file ID is not set") - self.client.file_attachments.delete(file_attachment=self.id_) + self.client.file_attachments.delete(file_attachments=self) def update(self, update: FileAttachmentUpdate | dict) -> FileAttachment: """Update the file attachment.""" From df19f311f0abf46e30722f751102c75865024065 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 11 Nov 2025 17:18:04 -0800 Subject: [PATCH 051/127] cleanup --- python/lib/sift_client/resources/file_attachments.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 82cd94f7a..0629d618c 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -159,9 +159,7 @@ async def get_download_url(self, *, file_attachment: FileAttachment | str) -> st raise ValueError("file_attachment must be a FileAttachment or a string") if id_ == "": raise ValueError("FileAttachment ID is not set") - return await self._low_level_client.get_remote_file_download_url( - remote_file_id=id_ - ) + return await self._low_level_client.get_remote_file_download_url(remote_file_id=id_) async def download( self, *, file_attachment: FileAttachment | str, output_path: str | Path From 97ab2c235fb35b7228585640574be56014c94276 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 12 Nov 2025 08:24:52 -0800 Subject: [PATCH 052/127] update interface --- .../resources/sync_stubs/__init__.pyi | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 6e2607109..891dd1d92 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -576,11 +576,11 @@ class FileAttachmentsAPI: """ ... - def delete(self, *, file_attachment_id: str) -> None: + def delete(self, *, file_attachments: list[FileAttachment | str] | FileAttachment | str) -> None: """Delete a file attachment. Args: - file_attachment_id: The ID of the file attachment to delete. + file_attachments: List of FileAttachments or the IDs of the file attachments to delete (up to 1000). """ ... @@ -592,17 +592,26 @@ class FileAttachmentsAPI: """ ... - def get_download_url(self, *, file_attachment_id: str) -> str: + def get_download_url(self, *, file_attachment: FileAttachment | str) -> str: """Get a download URL for a file attachment. Args: - file_attachment_id: The ID of the file attachment. + file_attachment: The FileAttachment or the ID of the file attachment. Returns: The download URL for the file attachment. """ ... + def download(self, *, file_attachment: FileAttachment | str, output_path: str | Path) -> None: + """Download a file attachment to a local path. + + Args: + file_attachment: The FileAttachment or the ID of the file attachment to download. + output_path: The path to download the file attachment to. + """ + ... + class PingAPI: """Sync counterpart to `PingAPIAsync`. From 8c1d9f4a5ca810f073e065d9911bc9b373aec34f Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 12 Nov 2025 12:24:25 -0800 Subject: [PATCH 053/127] format --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 891dd1d92..303a091c8 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -576,7 +576,9 @@ class FileAttachmentsAPI: """ ... - def delete(self, *, file_attachments: list[FileAttachment | str] | FileAttachment | str) -> None: + def delete( + self, *, file_attachments: list[FileAttachment | str] | FileAttachment | str + ) -> None: """Delete a file attachment. Args: From b4e33528634fcbebf95e2753246dedda6b4fbe64 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 12 Nov 2025 12:42:46 -0800 Subject: [PATCH 054/127] cleanup --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 4 ++-- python/lib/sift_client/sift_types/_mixins/file_attachments.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 303a091c8..eee442983 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -21,7 +21,7 @@ from sift_client.sift_types.calculated_channel import ( CalculatedChannelUpdate, ) from sift_client.sift_types.channel import Channel -from sift_client.sift_types.file_attachment import FileAttachment, RemoteFileUpdate +from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate from sift_client.sift_types.report import Report, ReportUpdate from sift_client.sift_types.rule import Rule, RuleCreate, RuleUpdate from sift_client.sift_types.run import Run, RunCreate, RunUpdate @@ -564,7 +564,7 @@ class FileAttachmentsAPI: def update( self, *, - file_attachment: RemoteFileUpdate | dict, + file_attachment: FileAttachmentUpdate | dict, ) -> FileAttachment: """Update a file attachment. diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 8fbbfb979..f5bebdc08 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -83,4 +83,4 @@ def delete_attachment( self.client.file_attachments.batch_delete(file_attachment_ids=file_ids) else: if file_attachment.id_: - self.client.file_attachments.delete(file_attachment_id=file_attachment.id_) + self.client.file_attachments.delete(file_attachment=file_attachment) From 2eeac8a80cd05c30d31c16c3e01840b5d4238d36 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 12 Nov 2025 12:56:07 -0800 Subject: [PATCH 055/127] cleanup --- python/lib/sift_client/sift_types/_mixins/file_attachments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index f5bebdc08..246502fc9 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -83,4 +83,4 @@ def delete_attachment( self.client.file_attachments.batch_delete(file_attachment_ids=file_ids) else: if file_attachment.id_: - self.client.file_attachments.delete(file_attachment=file_attachment) + self.client.file_attachments.delete(file_attachments=file_attachment) From 8cf30a3088c4b84447a8d682c81e45fe0481f077 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 12 Nov 2025 13:12:04 -0800 Subject: [PATCH 056/127] cleanup batch delete --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 8 -------- .../sift_client/sift_types/_mixins/file_attachments.py | 8 +------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index eee442983..17919267b 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -586,14 +586,6 @@ class FileAttachmentsAPI: """ ... - def batch_delete(self, *, file_attachment_ids: list[str]) -> None: - """Batch delete multiple file attachments. - - Args: - file_attachment_ids: List of file attachment IDs to delete (up to 1000). - """ - ... - def get_download_url(self, *, file_attachment: FileAttachment | str) -> str: """Get a download URL for a file attachment. diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 246502fc9..0064ea55f 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -77,10 +77,4 @@ def delete_attachment( Args: file_attachment: A single FileAttachment or list of FileAttachments to delete. """ - if isinstance(file_attachment, list): - file_ids = [fa.id_ for fa in file_attachment if fa.id_] - if file_ids: - self.client.file_attachments.batch_delete(file_attachment_ids=file_ids) - else: - if file_attachment.id_: - self.client.file_attachments.delete(file_attachments=file_attachment) + self.client.file_attachments.delete(file_attachments=file_attachment) From 6ad71d9421237ae4013eca35d741aa3c66227a48 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 12 Nov 2025 14:27:46 -0800 Subject: [PATCH 057/127] cleanup --- python/lib/sift_client/sift_types/_mixins/file_attachments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 0064ea55f..acea7705c 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -70,7 +70,7 @@ def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]: ) def delete_attachment( - self: _SupportsFileAttachments, file_attachment: FileAttachment | list[FileAttachment] + self: _SupportsFileAttachments, file_attachment: list[FileAttachment] | FileAttachment | str ) -> None: """Delete one or more file attachments. From df3f77f72f5d36f95e724c426b634ee3cb27a79e Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 12 Nov 2025 14:42:30 -0800 Subject: [PATCH 058/127] cleanup --- python/lib/sift_client/sift_types/_mixins/file_attachments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index acea7705c..8c8715f19 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -70,7 +70,7 @@ def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]: ) def delete_attachment( - self: _SupportsFileAttachments, file_attachment: list[FileAttachment] | FileAttachment | str + self: _SupportsFileAttachments, file_attachment: list[FileAttachment | str] | FileAttachment | str ) -> None: """Delete one or more file attachments. From 2d91c10fad0560c4d01d0d031876a45e2718ac74 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 12 Nov 2025 14:44:02 -0800 Subject: [PATCH 059/127] cleanup --- python/lib/sift_client/sift_types/_mixins/file_attachments.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 8c8715f19..55addc36f 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -70,7 +70,8 @@ def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]: ) def delete_attachment( - self: _SupportsFileAttachments, file_attachment: list[FileAttachment | str] | FileAttachment | str + self: _SupportsFileAttachments, + file_attachment: list[FileAttachment | str] | FileAttachment | str, ) -> None: """Delete one or more file attachments. From efbbf291c834c18ef1a76242559f78bbec48da97 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 12 Nov 2025 16:46:34 -0800 Subject: [PATCH 060/127] add file uploads --- .../sift_client/resources/file_attachments.py | 34 ++++++++++++++++++- .../sift_types/_mixins/file_attachments.py | 30 +++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 0629d618c..e73f1951d 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -5,7 +5,9 @@ from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient from sift_client.resources._base import ResourceBase -from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate +from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate, Metadata + +from sift_py.file_attachment.entity import Entity if TYPE_CHECKING: from sift_client.client import SiftClient @@ -174,3 +176,33 @@ async def download( download_url = await self.get_download_url(file_attachment=file_attachment) download_remote_file(download_url, Path(output_path)) + + + async def upload( + self, + *, + path: str | Path, + entity: Entity, + metadata: Metadata | None = None, + description: str | None = None, + organization_id: str | None = None, + ) -> FileAttachment: + """Upload a file attachment to a remote file. + + Args: + path: The path to the file to upload. + entity: The entity to attach the file to. + metadata: Optional metadata for the file (e.g., video/image metadata). + description: Optional description of the file. + organization_id: Optional organization ID. + + Returns: + The uploaded FileAttachment. + """ + return await self._low_level_client.upload_attachment( + path=path, + entity=entity, + metadata=metadata, + description=description, + organization_id=organization_id, + ) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 55addc36f..3476a63ab 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -3,8 +3,10 @@ from typing import TYPE_CHECKING, ClassVar, Protocol if TYPE_CHECKING: + from pathlib import Path + from sift_client.client import SiftClient - from sift_client.sift_types.file_attachment import FileAttachment + from sift_client.sift_types.file_attachment import FileAttachment, Metadata class _SupportsFileAttachments(Protocol): @@ -79,3 +81,29 @@ def delete_attachment( file_attachment: A single FileAttachment or list of FileAttachments to delete. """ self.client.file_attachments.delete(file_attachments=file_attachment) + + def upload_attachment( + self: _SupportsFileAttachments, + path: str | Path, + metadata: Metadata | None = None, + description: str | None = None, + organization_id: str | None = None, + ) -> FileAttachment: + """Upload a file attachment to a remote file. + + Args: + path: The path to the file to upload. + metadata: Optional metadata for the file (e.g., video/image metadata). + description: Optional description of the file. + organization_id: Optional organization ID. + + Returns: + The uploaded FileAttachment. + """ + return self.client.file_attachments.upload( + path=path, + entity=self, + metadata=metadata, + description=description, + organization_id=organization_id, + ) From c27086a79eab21f50c3065d21dadaabab8be7369 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 12 Nov 2025 16:52:24 -0800 Subject: [PATCH 061/127] ruff check --- python/lib/sift_client/resources/file_attachments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index e73f1951d..7a60fc7b2 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -7,9 +7,9 @@ from sift_client.resources._base import ResourceBase from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate, Metadata -from sift_py.file_attachment.entity import Entity - if TYPE_CHECKING: + from sift_py.file_attachment.entity import Entity + from sift_client.client import SiftClient From a6faae628f582584430cc787c819d9719293844e Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 12 Nov 2025 17:34:25 -0800 Subject: [PATCH 062/127] format --- python/lib/sift_client/resources/file_attachments.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 7a60fc7b2..133fc8974 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -177,7 +177,6 @@ async def download( download_url = await self.get_download_url(file_attachment=file_attachment) download_remote_file(download_url, Path(output_path)) - async def upload( self, *, From b9cb9831450aabf45d5ab49490efb9a80edc5cbd Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 13 Nov 2025 14:11:36 -0800 Subject: [PATCH 063/127] fix types --- .../sift_client/resources/file_attachments.py | 17 +++++++++---- .../resources/sync_stubs/__init__.pyi | 25 +++++++++++++++++++ .../sift_types/_mixins/file_attachments.py | 15 ++++++++--- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 133fc8974..29e56f3f8 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -1,16 +1,18 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient from sift_client.resources._base import ResourceBase -from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate, Metadata +from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate +from sift_client._internal.low_level_wrappers.upload import UploadLowLevelClient if TYPE_CHECKING: from sift_py.file_attachment.entity import Entity from sift_client.client import SiftClient + from sift_py.file_attachment.metadata import Metadata class FileAttachmentsAPIAsync(ResourceBase): @@ -28,6 +30,7 @@ def __init__(self, sift_client: SiftClient): """ super().__init__(sift_client) self._low_level_client = RemoteFilesLowLevelClient(grpc_client=self.client.grpc_client) + self._upload_client = UploadLowLevelClient(rest_client=self.client.rest_client) async def get(self, *, file_attachment_id: str) -> FileAttachment: """Get a file attachment by ID. @@ -182,7 +185,8 @@ async def upload( *, path: str | Path, entity: Entity, - metadata: Metadata | None = None, + entity_type: str, + metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, ) -> FileAttachment: @@ -198,10 +202,13 @@ async def upload( Returns: The uploaded FileAttachment. """ - return await self._low_level_client.upload_attachment( + remote_file_id = await self._upload_client.upload_attachment( path=path, - entity=entity, + entity_id=entity.entity_id, + entity_type=entity_type, metadata=metadata, description=description, organization_id=organization_id, ) + # Should be able to remove await + return await self.get(file_attachment_id=remote_file_id) \ No newline at end of file diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 17919267b..c31169128 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -40,6 +40,8 @@ from sift_client.sift_types.test_report import ( TestStepType, TestStepUpdate, ) +from sift_py.file_attachment.entity import Entity +from sift_py.file_attachment.metadata import Metadata class AssetsAPI: """Sync counterpart to `AssetsAPIAsync`. @@ -606,6 +608,29 @@ class FileAttachmentsAPI: """ ... + def upload( + self, + *, + path: str | Path, + entity: Entity, + metadata: dict[str, Any] | None = None, + description: str | None = None, + organization_id: str | None = None, + ) -> FileAttachment: + """Upload a file attachment to a remote file. + + Args: + path: The path to the file to upload. + entity: The entity to attach the file to. + metadata: Optional metadata for the file (e.g., video/image metadata). + description: Optional description of the file. + organization_id: Optional organization ID. + + Returns: + The uploaded FileAttachment. + """ + ... + class PingAPI: """Sync counterpart to `PingAPIAsync`. diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 3476a63ab..991190926 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -1,12 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, Protocol +from typing import TYPE_CHECKING, Any, ClassVar, Protocol +from sift_py.file_attachment.entity import Entity if TYPE_CHECKING: from pathlib import Path from sift_client.client import SiftClient - from sift_client.sift_types.file_attachment import FileAttachment, Metadata + from sift_client.sift_types.file_attachment import FileAttachment class _SupportsFileAttachments(Protocol): @@ -85,7 +86,7 @@ def delete_attachment( def upload_attachment( self: _SupportsFileAttachments, path: str | Path, - metadata: Metadata | None = None, + metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, ) -> FileAttachment: @@ -100,9 +101,15 @@ def upload_attachment( Returns: The uploaded FileAttachment. """ + if not self.id_: + raise ValueError("Entity ID is not set") + entity = Entity( + entity_id=self.id_, + entity_type=self._get_entity_type_name(), # type: ignore[attr-defined] + ) return self.client.file_attachments.upload( path=path, - entity=self, + entity=entity, metadata=metadata, description=description, organization_id=organization_id, From 8f9f6120a0aa5a778a118b1d5849066d5ecbc632 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 13 Nov 2025 14:14:18 -0800 Subject: [PATCH 064/127] format --- python/lib/sift_client/resources/file_attachments.py | 6 +++--- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 4 ++-- .../lib/sift_client/sift_types/_mixins/file_attachments.py | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 29e56f3f8..db91ac14b 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -4,15 +4,14 @@ from typing import TYPE_CHECKING, Any from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient +from sift_client._internal.low_level_wrappers.upload import UploadLowLevelClient from sift_client.resources._base import ResourceBase from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate -from sift_client._internal.low_level_wrappers.upload import UploadLowLevelClient if TYPE_CHECKING: from sift_py.file_attachment.entity import Entity from sift_client.client import SiftClient - from sift_py.file_attachment.metadata import Metadata class FileAttachmentsAPIAsync(ResourceBase): @@ -195,6 +194,7 @@ async def upload( Args: path: The path to the file to upload. entity: The entity to attach the file to. + entity_type: The type of entity (e.g., "runs", "annotations", "annotation_logs"). metadata: Optional metadata for the file (e.g., video/image metadata). description: Optional description of the file. organization_id: Optional organization ID. @@ -211,4 +211,4 @@ async def upload( organization_id=organization_id, ) # Should be able to remove await - return await self.get(file_attachment_id=remote_file_id) \ No newline at end of file + return await self.get(file_attachment_id=remote_file_id) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index c31169128..42387fb11 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -13,6 +13,8 @@ if TYPE_CHECKING: import pandas as pd import pyarrow as pa +from sift_py.file_attachment.entity import Entity + from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset, AssetUpdate from sift_client.sift_types.calculated_channel import ( @@ -40,8 +42,6 @@ from sift_client.sift_types.test_report import ( TestStepType, TestStepUpdate, ) -from sift_py.file_attachment.entity import Entity -from sift_py.file_attachment.metadata import Metadata class AssetsAPI: """Sync counterpart to `AssetsAPIAsync`. diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 991190926..41d242f92 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, ClassVar, Protocol + from sift_py.file_attachment.entity import Entity if TYPE_CHECKING: From e99170e350bfdd703f92098f55c0871a826a5203 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 13 Nov 2025 15:11:29 -0800 Subject: [PATCH 065/127] small fix --- python/lib/sift_client/resources/file_attachments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index db91ac14b..1a17f599e 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -193,7 +193,7 @@ async def upload( Args: path: The path to the file to upload. - entity: The entity to attach the file to. + entity: The entity that the file is attached to. entity_type: The type of entity (e.g., "runs", "annotations", "annotation_logs"). metadata: Optional metadata for the file (e.g., video/image metadata). description: Optional description of the file. From 7f8e2c9422dd793cd1ab19b95b1bac720725208a Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 09:41:18 -0800 Subject: [PATCH 066/127] Update sync stubs for file attachments API --- .../resources/sync_stubs/__init__.pyi | 133 +++++++++--------- 1 file changed, 65 insertions(+), 68 deletions(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 42387fb11..47ce5ad1c 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -12,36 +12,35 @@ if TYPE_CHECKING: import pandas as pd import pyarrow as pa - -from sift_py.file_attachment.entity import Entity - -from sift_client.client import SiftClient -from sift_client.sift_types.asset import Asset, AssetUpdate -from sift_client.sift_types.calculated_channel import ( - CalculatedChannel, - CalculatedChannelCreate, - CalculatedChannelUpdate, -) -from sift_client.sift_types.channel import Channel -from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate -from sift_client.sift_types.report import Report, ReportUpdate -from sift_client.sift_types.rule import Rule, RuleCreate, RuleUpdate -from sift_client.sift_types.run import Run, RunCreate, RunUpdate -from sift_client.sift_types.tag import Tag, TagUpdate -from sift_client.sift_types.test_report import ( - TestMeasurement, - TestMeasurementCreate, - TestMeasurementType, - TestMeasurementUpdate, - TestReport, - TestReportCreate, - TestReportUpdate, - TestStatus, - TestStep, - TestStepCreate, - TestStepType, - TestStepUpdate, -) + from sift_py.file_attachment.entity import Entity + + from sift_client.client import SiftClient + from sift_client.sift_types.asset import Asset, AssetUpdate + from sift_client.sift_types.calculated_channel import ( + CalculatedChannel, + CalculatedChannelCreate, + CalculatedChannelUpdate, + ) + from sift_client.sift_types.channel import Channel + from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate + from sift_client.sift_types.report import Report, ReportUpdate + from sift_client.sift_types.rule import Rule, RuleCreate, RuleUpdate + from sift_client.sift_types.run import Run, RunCreate, RunUpdate + from sift_client.sift_types.tag import Tag, TagUpdate + from sift_client.sift_types.test_report import ( + TestMeasurement, + TestMeasurementCreate, + TestMeasurementType, + TestMeasurementUpdate, + TestReport, + TestReportCreate, + TestReportUpdate, + TestStatus, + TestStep, + TestStepCreate, + TestStepType, + TestStepUpdate, + ) class AssetsAPI: """Sync counterpart to `AssetsAPIAsync`. @@ -519,7 +518,7 @@ class FileAttachmentsAPI: """ def __init__(self, sift_client: SiftClient): - """Initialize the FileAttachmentsAPI. + """Initialize the FileAttachmentsAPIAsync. Args: sift_client: The Sift client to use. @@ -527,6 +526,25 @@ class FileAttachmentsAPI: ... def _run(self, coro): ... + def delete( + self, *, file_attachments: list[FileAttachment | str] | FileAttachment | str + ) -> None: + """Batch delete multiple file attachments. + + Args: + file_attachments: List of FileAttachments or the IDs of the file attachments to delete (up to 1000). + """ + ... + + def download(self, *, file_attachment: FileAttachment | str, output_path: str | Path) -> None: + """Download a file attachment to a local path. + + Args: + file_attachment: The FileAttachment or the ID of the file attachment to download. + output_path: The path to download the file attachment to. + """ + ... + def get(self, *, file_attachment_id: str) -> FileAttachment: """Get a file attachment by ID. @@ -538,6 +556,17 @@ class FileAttachmentsAPI: """ ... + def get_download_url(self, *, file_attachment: FileAttachment | str) -> str: + """Get a download URL for a file attachment. + + Args: + file_attachment: The FileAttachment or the ID of the file attachment. + + Returns: + The download URL for the file attachment. + """ + ... + def list_( self, *, @@ -563,56 +592,23 @@ class FileAttachmentsAPI: """ ... - def update( - self, - *, - file_attachment: FileAttachmentUpdate | dict, - ) -> FileAttachment: + def update(self, *, file_attachment: FileAttachmentUpdate | dict) -> FileAttachment: """Update a file attachment. Args: - file_attachment: The RemoteFileUpdate with fields to update. + file_attachment: The FileAttachmentUpdate with fields to update. Returns: The updated FileAttachment. """ ... - def delete( - self, *, file_attachments: list[FileAttachment | str] | FileAttachment | str - ) -> None: - """Delete a file attachment. - - Args: - file_attachments: List of FileAttachments or the IDs of the file attachments to delete (up to 1000). - """ - ... - - def get_download_url(self, *, file_attachment: FileAttachment | str) -> str: - """Get a download URL for a file attachment. - - Args: - file_attachment: The FileAttachment or the ID of the file attachment. - - Returns: - The download URL for the file attachment. - """ - ... - - def download(self, *, file_attachment: FileAttachment | str, output_path: str | Path) -> None: - """Download a file attachment to a local path. - - Args: - file_attachment: The FileAttachment or the ID of the file attachment to download. - output_path: The path to download the file attachment to. - """ - ... - def upload( self, *, path: str | Path, entity: Entity, + entity_type: str, metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, @@ -621,7 +617,8 @@ class FileAttachmentsAPI: Args: path: The path to the file to upload. - entity: The entity to attach the file to. + entity: The entity that the file is attached to. + entity_type: The type of entity (e.g., "runs", "annotations", "annotation_logs"). metadata: Optional metadata for the file (e.g., video/image metadata). description: Optional description of the file. organization_id: Optional organization ID. From a573ae67ffa0f2e8d665bb0790e066e42c1faced Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 09:43:26 -0800 Subject: [PATCH 067/127] cleanup --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 1 - 1 file changed, 1 deletion(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 47ce5ad1c..5006a85c9 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -525,7 +525,6 @@ class FileAttachmentsAPI: """ ... - def _run(self, coro): ... def delete( self, *, file_attachments: list[FileAttachment | str] | FileAttachment | str ) -> None: From bec37f327dd2fe4ecdba3bac137b5afae6ef0e1d Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 10:54:25 -0800 Subject: [PATCH 068/127] cleanup --- python/lib/sift_client/resources/file_attachments.py | 3 +-- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 1a17f599e..e55e0ee73 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -184,7 +184,6 @@ async def upload( *, path: str | Path, entity: Entity, - entity_type: str, metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, @@ -205,7 +204,7 @@ async def upload( remote_file_id = await self._upload_client.upload_attachment( path=path, entity_id=entity.entity_id, - entity_type=entity_type, + entity_type=entity.entity_type.value, metadata=metadata, description=description, organization_id=organization_id, diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 5006a85c9..219ec882e 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -607,7 +607,6 @@ class FileAttachmentsAPI: *, path: str | Path, entity: Entity, - entity_type: str, metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, From dce8c411da668ea225853eb5d7b88dc7595edd56 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 11:13:53 -0800 Subject: [PATCH 069/127] Add missing _run method to FileAttachmentsAPI stubs --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 219ec882e..a077f8626 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -525,6 +525,7 @@ class FileAttachmentsAPI: """ ... + def _run(self, coro): ... def delete( self, *, file_attachments: list[FileAttachment | str] | FileAttachment | str ) -> None: From 951b24037e978f0bd9b5f354233f8acddc0c21c9 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 13:27:05 -0800 Subject: [PATCH 070/127] restore rust files --- .../sift_stream_bindings.pyi | 218 +++++------------- .../sift_stream_bindings/tests/test_stream.py | 28 +-- .../sift_stream_bindings/tests/test_value.py | 117 ---------- 3 files changed, 61 insertions(+), 302 deletions(-) delete mode 100644 rust/crates/sift_stream_bindings/tests/test_value.py diff --git a/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi b/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi index 1df085bc9..788bc20cd 100644 --- a/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi +++ b/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi @@ -1,53 +1,30 @@ # This file is automatically generated by pyo3_stub_gen __all__ = [ - "BackupMetricsSnapshotPy", "ChannelBitFieldElementPy", "ChannelConfigPy", "ChannelDataTypePy", - "ChannelEnumPy", "ChannelEnumTypePy", "ChannelValuePy", "ChannelValueTypePy", - "CheckpointMetricsSnapshotPy", - "DiskBackupPolicyPy", "DurationPy", "FlowConfigPy", "FlowPy", "IngestWithConfigDataChannelValuePy", "IngestWithConfigDataStreamRequestPy", "IngestionConfigFormPy", - "MetadataPy", - "MetadataValuePy", "RecoveryStrategyPy", "RetryPolicyPy", - "RollingFilePolicyPy", "RunFormPy", - "RunSelectorPy", "SiftStreamBuilderPy", - "SiftStreamMetricsSnapshotPy", "SiftStreamPy", "TimeValuePy", - "ValuePy", ] import builtins import typing from enum import Enum -@typing.final -class BackupMetricsSnapshotPy: - cur_checkpoint_file_count: builtins.int - cur_checkpoint_cur_file_size: builtins.int - cur_checkpoint_bytes: builtins.int - cur_checkpoint_messages: builtins.int - total_file_count: builtins.int - total_bytes: builtins.int - total_messages: builtins.int - files_pending_ingestion: builtins.int - files_ingested: builtins.int - cur_ingest_retries: builtins.int - @typing.final class ChannelBitFieldElementPy: name: builtins.str @@ -75,10 +52,6 @@ class ChannelConfigPy: bit_field_elements: typing.Sequence[ChannelBitFieldElementPy], ) -> ChannelConfigPy: ... -@typing.final -class ChannelEnumPy: - def __new__(cls, val: builtins.int) -> ChannelEnumPy: ... - @typing.final class ChannelEnumTypePy: name: builtins.str @@ -87,9 +60,28 @@ class ChannelEnumTypePy: @typing.final class ChannelValuePy: - name: builtins.str - value: ValuePy - def __new__(cls, name: builtins.str, value: ValuePy) -> ChannelValuePy: ... + @staticmethod + def bool(name: builtins.str, value: builtins.bool) -> ChannelValuePy: ... + @staticmethod + def string(name: builtins.str, value: builtins.str) -> ChannelValuePy: ... + @staticmethod + def float(name: builtins.str, value: builtins.float) -> ChannelValuePy: ... + @staticmethod + def double(name: builtins.str, value: builtins.float) -> ChannelValuePy: ... + @staticmethod + def int32(name: builtins.str, value: builtins.int) -> ChannelValuePy: ... + @staticmethod + def uint32(name: builtins.str, value: builtins.int) -> ChannelValuePy: ... + @staticmethod + def int64(name: builtins.str, value: builtins.int) -> ChannelValuePy: ... + @staticmethod + def uint64(name: builtins.str, value: builtins.int) -> ChannelValuePy: ... + @staticmethod + def enum_value(name: builtins.str, value: ChannelEnumTypePy) -> ChannelValuePy: ... + @staticmethod + def bitfield( + name: builtins.str, value: typing.Sequence[builtins.int] + ) -> ChannelValuePy: ... @typing.final class ChannelValueTypePy: @@ -118,34 +110,6 @@ class ChannelValueTypePy: @staticmethod def empty() -> ChannelValueTypePy: ... -@typing.final -class CheckpointMetricsSnapshotPy: - checkpoint_count: builtins.int - failed_checkpoint_count: builtins.int - checkpoint_timer_reached_cnt: builtins.int - checkpoint_manually_reached_cnt: builtins.int - cur_elapsed_secs: builtins.float - cur_messages_sent: builtins.int - cur_message_rate: builtins.float - cur_bytes_sent: builtins.int - cur_byte_rate: builtins.float - -@typing.final -class DiskBackupPolicyPy: - backups_dir: typing.Optional[builtins.str] - max_backup_file_size: builtins.int - rolling_file_policy: RollingFilePolicyPy - retain_backups: builtins.bool - def __new__( - cls, - backups_dir: typing.Optional[builtins.str], - max_backup_file_size: builtins.int, - rolling_file_policy: RollingFilePolicyPy, - retain_backups: builtins.bool, - ) -> DiskBackupPolicyPy: ... - @staticmethod - def default() -> DiskBackupPolicyPy: ... - @typing.final class DurationPy: secs: builtins.int @@ -171,7 +135,6 @@ class FlowPy: @typing.final class IngestWithConfigDataChannelValuePy: - ty: ChannelValueTypePy @staticmethod def bool(value: builtins.bool) -> IngestWithConfigDataChannelValuePy: ... @staticmethod @@ -229,22 +192,39 @@ class IngestionConfigFormPy: flows: typing.Sequence[FlowConfigPy], ) -> IngestionConfigFormPy: ... -@typing.final -class MetadataPy: - key: builtins.str - value: MetadataValuePy - def __new__(cls, key: builtins.str, value: MetadataValuePy) -> MetadataPy: ... - @typing.final class RecoveryStrategyPy: + strategy_type: builtins.str + retry_policy: typing.Optional[RetryPolicyPy] + max_buffer_size: typing.Optional[builtins.int] + backups_dir: typing.Optional[builtins.str] + max_backups_file_size: typing.Optional[builtins.int] + def __new__( + cls, + strategy_type: builtins.str, + retry_policy: typing.Optional[RetryPolicyPy], + max_buffer_size: typing.Optional[builtins.int], + backups_dir: typing.Optional[builtins.str], + max_backups_file_size: typing.Optional[builtins.int], + ) -> RecoveryStrategyPy: ... @staticmethod def retry_only(retry_policy: RetryPolicyPy) -> RecoveryStrategyPy: ... @staticmethod - def retry_with_backups( - retry_policy: RetryPolicyPy, disk_backup_policy: DiskBackupPolicyPy + def retry_with_in_memory_backups( + retry_policy: RetryPolicyPy, max_buffer_size: typing.Optional[builtins.int] + ) -> RecoveryStrategyPy: ... + @staticmethod + def retry_with_disk_backups( + retry_policy: RetryPolicyPy, + backups_dir: typing.Optional[builtins.str], + max_backups_file_size: typing.Optional[builtins.int], ) -> RecoveryStrategyPy: ... @staticmethod def default() -> RecoveryStrategyPy: ... + @staticmethod + def default_retry_policy_in_memory_backups() -> RecoveryStrategyPy: ... + @staticmethod + def default_retry_policy_disk_backups() -> RecoveryStrategyPy: ... @typing.final class RetryPolicyPy: @@ -262,37 +242,20 @@ class RetryPolicyPy: @staticmethod def default() -> RetryPolicyPy: ... -@typing.final -class RollingFilePolicyPy: - def __new__( - cls, max_file_count: typing.Optional[builtins.int] - ) -> RollingFilePolicyPy: ... - @staticmethod - def default() -> RollingFilePolicyPy: ... - @typing.final class RunFormPy: name: builtins.str client_key: builtins.str description: typing.Optional[builtins.str] tags: typing.Optional[builtins.list[builtins.str]] - metadata: typing.Optional[builtins.list[MetadataPy]] def __new__( cls, name: builtins.str, client_key: builtins.str, description: typing.Optional[builtins.str], tags: typing.Optional[typing.Sequence[builtins.str]], - metadata: typing.Optional[typing.Sequence[MetadataPy]], ) -> RunFormPy: ... -@typing.final -class RunSelectorPy: - @staticmethod - def by_id(run_id: builtins.str) -> RunSelectorPy: ... - @staticmethod - def by_form(form: RunFormPy) -> RunSelectorPy: ... - @typing.final class SiftStreamBuilderPy: uri: builtins.str @@ -300,42 +263,20 @@ class SiftStreamBuilderPy: enable_tls: builtins.bool ingestion_config: typing.Optional[IngestionConfigFormPy] recovery_strategy: typing.Optional[RecoveryStrategyPy] - checkpoint_interval: typing.Optional[DurationPy] + checkpoint_interval: DurationPy run: typing.Optional[RunFormPy] run_id: typing.Optional[builtins.str] - asset_tags: typing.Optional[builtins.list[builtins.str]] - metadata: typing.Optional[builtins.list[MetadataPy]] def __new__( cls, uri: builtins.str, apikey: builtins.str ) -> SiftStreamBuilderPy: ... def build(self) -> typing.Any: ... -@typing.final -class SiftStreamMetricsSnapshotPy: - elapsed_secs: builtins.float - loaded_flows: builtins.int - unique_flows_received: builtins.int - messages_received: builtins.int - messages_sent: builtins.int - message_rate: builtins.float - bytes_sent: builtins.int - byte_rate: builtins.float - messages_sent_to_backup: builtins.int - cur_retry_count: builtins.int - checkpoint: CheckpointMetricsSnapshotPy - backups: BackupMetricsSnapshotPy - @typing.final class SiftStreamPy: - def send(self, flow:FlowPy) -> typing.Any: ... - def batch_send(self, flows:typing.Any) -> typing.Any: ... - def send_requests(self, requests:typing.Sequence[IngestWithConfigDataStreamRequestPy]) -> typing.Any: ... - def get_metrics_snapshot(self) -> SiftStreamMetricsSnapshotPy: ... - def add_new_flows(self, flow_configs:typing.Sequence[FlowConfigPy]) -> typing.Any: ... - def get_flows(self) -> builtins.dict[builtins.str, FlowConfigPy]: ... - def attach_run(self, run_selector:RunSelectorPy) -> typing.Any: ... - def detach_run(self) -> None: ... - def run(self) -> typing.Optional[builtins.str]: ... + def send(self, flow: FlowPy) -> typing.Any: ... + def send_requests( + self, requests: typing.Sequence[IngestWithConfigDataStreamRequestPy] + ) -> typing.Any: ... def finish(self) -> typing.Any: ... @typing.final @@ -352,49 +293,6 @@ class TimeValuePy: @staticmethod def from_rfc3339(val: builtins.str) -> TimeValuePy: ... -@typing.final -class ValuePy: - @staticmethod - def Bool(value: builtins.bool) -> ValuePy: ... - @staticmethod - def String(value: builtins.str) -> ValuePy: ... - @staticmethod - def Float(value: builtins.float) -> ValuePy: ... - @staticmethod - def Double(value: builtins.float) -> ValuePy: ... - @staticmethod - def Int32(value: builtins.int) -> ValuePy: ... - @staticmethod - def Int64(value: builtins.int) -> ValuePy: ... - @staticmethod - def Uint32(value: builtins.int) -> ValuePy: ... - @staticmethod - def Uint64(value: builtins.int) -> ValuePy: ... - @staticmethod - def Enum(value: builtins.int) -> ValuePy: ... - @staticmethod - def BitField(value: typing.Sequence[builtins.int]) -> ValuePy: ... - def is_bool(self) -> builtins.bool: ... - def is_string(self) -> builtins.bool: ... - def is_float(self) -> builtins.bool: ... - def is_double(self) -> builtins.bool: ... - def is_int32(self) -> builtins.bool: ... - def is_int64(self) -> builtins.bool: ... - def is_uint32(self) -> builtins.bool: ... - def is_uint64(self) -> builtins.bool: ... - def is_enum(self) -> builtins.bool: ... - def is_bitfield(self) -> builtins.bool: ... - def as_bool(self) -> builtins.bool: ... - def as_string(self) -> builtins.str: ... - def as_float(self) -> builtins.float: ... - def as_double(self) -> builtins.float: ... - def as_int32(self) -> builtins.int: ... - def as_int64(self) -> builtins.int: ... - def as_uint32(self) -> builtins.int: ... - def as_uint64(self) -> builtins.int: ... - def as_enum(self) -> builtins.int: ... - def as_bitfield(self) -> builtins.list[builtins.int]: ... - @typing.final class ChannelDataTypePy(Enum): Unspecified = ... @@ -409,15 +307,3 @@ class ChannelDataTypePy(Enum): Int64 = ... Uint64 = ... Bytes = ... - -@typing.final -class MetadataValuePy(Enum): - String = ... - Number = ... - Boolean = ... - - def __new__(cls, obj: typing.Any) -> MetadataValuePy: ... - def __str__(self) -> builtins.str: ... - def is_string(self) -> builtins.bool: ... - def is_number(self) -> builtins.bool: ... - def is_boolean(self) -> builtins.bool: ... diff --git a/rust/crates/sift_stream_bindings/tests/test_stream.py b/rust/crates/sift_stream_bindings/tests/test_stream.py index cd83bedf9..648b30499 100644 --- a/rust/crates/sift_stream_bindings/tests/test_stream.py +++ b/rust/crates/sift_stream_bindings/tests/test_stream.py @@ -4,14 +4,14 @@ from sift_stream_bindings import ( ChannelConfigPy, ChannelDataTypePy, + ChannelValuePy, FlowConfigPy, FlowPy, IngestionConfigFormPy, + IngestWithConfigDataStreamRequestPy, RunFormPy, SiftStreamBuilderPy, TimeValuePy, - ValuePy, - IngestWithConfigDataStreamRequestPy, ) @@ -26,14 +26,12 @@ def test_create_empty_flow(self): def test_create_flow_with_multiple_values(self): """Test creating a flow with multiple channel values.""" - from sift_stream_bindings import ChannelValuePy - timestamp = TimeValuePy.from_timestamp(int(time.time()), 0) values = [ - ChannelValuePy("temperature", ValuePy.Float(23.5)), - ChannelValuePy("active", ValuePy.Bool(True)), - ChannelValuePy("status", ValuePy.String("running")), - ChannelValuePy("count", ValuePy.Int32(42)), + ChannelValuePy.float("temperature", 23.5), + ChannelValuePy.bool("active", True), + ChannelValuePy.string("status", "running"), + ChannelValuePy.int32("count", 42), ] flow = FlowPy("test_flow", timestamp, values) assert flow @@ -51,7 +49,8 @@ def test_create_stream_builder(self): assert builder.enable_tls is True assert builder.ingestion_config is None assert builder.recovery_strategy is None - assert builder.checkpoint_interval is None + assert builder.checkpoint_interval.secs == 60 + assert builder.checkpoint_interval.nanos == 0 def test_set_ingestion_config(self): """Test setting ingestion config on builder.""" @@ -88,20 +87,13 @@ def test_set_ingestion_config(self): def test_set_run_form(self): """Test setting run form on builder.""" - from sift_stream_bindings import MetadataPy, MetadataValuePy - builder = SiftStreamBuilderPy("https://api.example.com", "test-api-key") - metadata = [ - MetadataPy(key="test_key", value=MetadataValuePy("test_value")), - ] - run_form = RunFormPy( name="Test Run", - client_key="test-run-key", description="Test run description", + client_key="test-run-key", tags=[], - metadata=metadata, ) builder.run = run_form @@ -110,8 +102,6 @@ def test_set_run_form(self): assert builder.run.description == "Test run description" assert builder.run.client_key == "test-run-key" assert builder.run.tags == [] - assert builder.run.metadata is not None - assert len(builder.run.metadata) == 1 @pytest.mark.asyncio async def test_build_stream_no_ingestion_config(self): diff --git a/rust/crates/sift_stream_bindings/tests/test_value.py b/rust/crates/sift_stream_bindings/tests/test_value.py deleted file mode 100644 index ccddeaa6f..000000000 --- a/rust/crates/sift_stream_bindings/tests/test_value.py +++ /dev/null @@ -1,117 +0,0 @@ -import pytest -from sift_stream_bindings import ValuePy - - -class TestValuePy: - """Test ValuePy functionality.""" - - def test_create_bool_value(self): - """Test creating boolean values.""" - val_true = ValuePy.Bool(True) - val_false = ValuePy.Bool(False) - - assert val_true.is_bool() - assert val_false.is_bool() - assert val_true.as_bool() is True - assert val_false.as_bool() is False - - def test_create_string_value(self): - """Test creating string values.""" - val = ValuePy.String("hello world") - - assert val.is_string() - assert val.as_string() == "hello world" - - def test_create_float_value(self): - """Test creating float values.""" - val = ValuePy.Float(3.14) - - assert val.is_float() - assert abs(val.as_float() - 3.14) < 0.001 - - def test_create_double_value(self): - """Test creating double values.""" - val = ValuePy.Double(3.141592653589793) - - assert val.is_double() - assert abs(val.as_double() - 3.141592653589793) < 1e-10 - - def test_create_int32_value(self): - """Test creating int32 values.""" - val = ValuePy.Int32(42) - - assert val.is_int32() - assert val.as_int32() == 42 - - def test_create_uint32_value(self): - """Test creating uint32 values.""" - val = ValuePy.Uint32(42) - - assert val.is_uint32() - assert val.as_uint32() == 42 - - def test_create_int64_value(self): - """Test creating int64 values.""" - val = ValuePy.Int64(9223372036854775807) - - assert val.is_int64() - assert val.as_int64() == 9223372036854775807 - - def test_create_uint64_value(self): - """Test creating uint64 values.""" - val = ValuePy.Uint64(18446744073709551615) - - assert val.is_uint64() - assert val.as_uint64() == 18446744073709551615 - - def test_create_enum_value(self): - """Test creating enum values.""" - val = ValuePy.Enum(2) - - assert val.is_enum() - assert val.as_enum() == 2 - - def test_create_bit_field_value(self): - """Test creating bit field values.""" - # BitField expects Vec, so pass list of bytes - val = ValuePy.BitField([0x0A]) - - assert val.is_bitfield() - assert val.as_bitfield() == b"\x0a" - - def test_type_error_on_wrong_accessor(self): - """Test that accessing with wrong type raises error.""" - val = ValuePy.Bool(True) - - with pytest.raises(TypeError): - val.as_string() - - with pytest.raises(TypeError): - val.as_int32() - - def test_multiple_values(self): - """Test creating multiple values of different types.""" - values = [ - ValuePy.Bool(True), - ValuePy.String("test"), - ValuePy.Float(1.5), - ValuePy.Double(2.5), - ValuePy.Int32(-100), - ValuePy.Uint32(100), - ValuePy.Int64(-1000000), - ValuePy.Uint64(1000000), - ValuePy.Enum(5), - ValuePy.BitField([0xFF]), - ] - - assert len(values) == 10 - assert values[0].is_bool() - assert values[1].is_string() - assert values[2].is_float() - assert values[3].is_double() - assert values[4].is_int32() - assert values[5].is_uint32() - assert values[6].is_int64() - assert values[7].is_uint64() - assert values[8].is_enum() - assert values[9].is_bitfield() From 8fa0b462fe22fdcf02dca372f7b1776cfd9d1bfe Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 13:54:56 -0800 Subject: [PATCH 071/127] remove delete from mixin file attachment Add more options to list file attachment --- .../low_level_wrappers/remote_files.py | 4 +- .../sift_client/resources/file_attachments.py | 52 +++++++------------ .../sift_client/sift_types/file_attachment.py | 6 --- 3 files changed, 21 insertions(+), 41 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 3412213f6..345b10506 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -60,7 +60,7 @@ async def get_remote_file( async def list_all_remote_files( self, - query_filter: str | None = None, + kwargs: dict[str, Any] | None = None, order_by: str | None = None, max_results: int | None = None, page_size: int | None = None, @@ -80,7 +80,7 @@ async def list_all_remote_files( """ return await self._handle_pagination( self.list_remote_files, - kwargs={"query_filter": query_filter, "sift_client": sift_client}, + kwargs=kwargs, page_size=page_size, order_by=order_by, max_results=max_results, diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index e55e0ee73..bd6612467 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -49,9 +49,10 @@ async def get(self, *, file_attachment_id: str) -> FileAttachment: async def list_( self, *, + remote_file_id: str | None = None, + file_name: str | None = None, entity_type: str | None = None, entity_id: str | None = None, - query_filter: str | None = None, order_by: str | None = None, limit: int | None = None, page_size: int | None = None, @@ -59,9 +60,10 @@ async def list_( """List file attachments with optional filtering. Args: + remote_file_id: Filter by remote file ID. + file_name: Filter by file name. entity_type: Filter by entity type (e.g., 'ENTITY_TYPE_ASSET', 'ENTITY_TYPE_RUN'). entity_id: Filter by entity ID. - query_filter: Optional CEL query filter. order_by: The field to order by. limit: Maximum number of results to return. page_size: Number of results per page. @@ -69,19 +71,18 @@ async def list_( Returns: A list of FileAttachments. """ - # Build the filter - filters = [] - if entity_type: - filters.append(f'entity_type=="{entity_type}"') + kwargs = {} + if remote_file_id: + kwargs["remote_file_id"] = remote_file_id + if file_name: + kwargs["file_name"] = file_name if entity_id: - filters.append(f'entity_id=="{entity_id}"') - if query_filter: - filters.append(query_filter) - - combined_filter = " && ".join(filters) if filters else None + kwargs["entity_id"] = entity_id + if entity_type: + kwargs["entity_type"] = entity_type file_attachments = await self._low_level_client.list_all_remote_files( - query_filter=combined_filter, + kwargs=kwargs, order_by=order_by, max_results=limit, page_size=page_size, @@ -121,25 +122,21 @@ async def delete( """ file_attachment_ids: list[str] = [] if isinstance(file_attachments, FileAttachment): - if file_attachments.id_ is not None: - file_attachment_ids.append(file_attachments.id_) - else: - raise ValueError("FileAttachment ID is not set") + file_attachment_ids.append(file_attachments._id_or_error) elif isinstance(file_attachments, str): file_attachment_ids.append(file_attachments) elif isinstance(file_attachments, list): for file_attachment in file_attachments: if isinstance(file_attachment, FileAttachment): - if file_attachment.id_ is not None: - file_attachment_ids.append(file_attachment.id_) - else: - raise ValueError("FileAttachment ID is not set") + file_attachment_ids.append(file_attachments._id_or_error) elif isinstance(file_attachment, str): file_attachment_ids.append(file_attachment) else: raise ValueError( "file_attachments must be a list of FileAttachment or list of str" ) + else: + raise ValueError("file_attachments must be a FileAttachment, a string, or a list of FileAttachment or strings") await self._low_level_client.batch_delete_remote_files(remote_file_ids=file_attachment_ids) async def get_download_url(self, *, file_attachment: FileAttachment | str) -> str: @@ -151,19 +148,8 @@ async def get_download_url(self, *, file_attachment: FileAttachment | str) -> st Returns: The download URL for the file attachment. """ - id_: str = "" - if isinstance(file_attachment, FileAttachment): - if file_attachment.id_ is not None: - id_ = file_attachment.id_ - else: - raise ValueError("FileAttachment ID is not set") - elif isinstance(file_attachment, str): - id_ = file_attachment - else: - raise ValueError("file_attachment must be a FileAttachment or a string") - if id_ == "": - raise ValueError("FileAttachment ID is not set") - return await self._low_level_client.get_remote_file_download_url(remote_file_id=id_) + attachment_id = file_attachment._id_or_error if isinstance(file_attachment, FileAttachment) else file_attachment + return await self._low_level_client.get_remote_file_download_url(remote_file_id=attachment_id) async def download( self, *, file_attachment: FileAttachment | str, output_path: str | Path diff --git a/python/lib/sift_client/sift_types/file_attachment.py b/python/lib/sift_client/sift_types/file_attachment.py index 2281ef769..518e740ad 100644 --- a/python/lib/sift_client/sift_types/file_attachment.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -113,12 +113,6 @@ def entity(self) -> Run | Asset | TestReport: else: raise Exception(f"Unknown remote file entity type: {self.entity_type}") - def delete(self) -> None: - """Delete the file attachment.""" - if self.id_ is None: - raise ValueError("Remote file ID is not set") - self.client.file_attachments.delete(file_attachments=self) - def update(self, update: FileAttachmentUpdate | dict) -> FileAttachment: """Update the file attachment.""" if isinstance(update, dict): From 80080e34bfcd16c6d0572f7dfe9ac5fa834f213d Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 14:09:47 -0800 Subject: [PATCH 072/127] format --- .../lib/sift_client/resources/file_attachments.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index bd6612467..3e865dd03 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -136,7 +136,9 @@ async def delete( "file_attachments must be a list of FileAttachment or list of str" ) else: - raise ValueError("file_attachments must be a FileAttachment, a string, or a list of FileAttachment or strings") + raise ValueError( + "file_attachments must be a FileAttachment, a string, or a list of FileAttachment or strings" + ) await self._low_level_client.batch_delete_remote_files(remote_file_ids=file_attachment_ids) async def get_download_url(self, *, file_attachment: FileAttachment | str) -> str: @@ -148,8 +150,14 @@ async def get_download_url(self, *, file_attachment: FileAttachment | str) -> st Returns: The download URL for the file attachment. """ - attachment_id = file_attachment._id_or_error if isinstance(file_attachment, FileAttachment) else file_attachment - return await self._low_level_client.get_remote_file_download_url(remote_file_id=attachment_id) + attachment_id = ( + file_attachment._id_or_error + if isinstance(file_attachment, FileAttachment) + else file_attachment + ) + return await self._low_level_client.get_remote_file_download_url( + remote_file_id=attachment_id + ) async def download( self, *, file_attachment: FileAttachment | str, output_path: str | Path From c65d283fd4bf2425aaa4b0ea3ff3274aeda34472 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 14:16:37 -0800 Subject: [PATCH 073/127] small fix --- python/lib/sift_client/resources/file_attachments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 3e865dd03..d264a41c4 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -128,7 +128,7 @@ async def delete( elif isinstance(file_attachments, list): for file_attachment in file_attachments: if isinstance(file_attachment, FileAttachment): - file_attachment_ids.append(file_attachments._id_or_error) + file_attachment_ids.append(file_attachment._id_or_error) elif isinstance(file_attachment, str): file_attachment_ids.append(file_attachment) else: From 2b265d1c72d124e4342707cc550d557aed8fde39 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 14:19:49 -0800 Subject: [PATCH 074/127] Update file attachments list_ method signature in stubs --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index a077f8626..df5dc08c4 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -570,9 +570,10 @@ class FileAttachmentsAPI: def list_( self, *, + remote_file_id: str | None = None, + file_name: str | None = None, entity_type: str | None = None, entity_id: str | None = None, - query_filter: str | None = None, order_by: str | None = None, limit: int | None = None, page_size: int | None = None, @@ -580,9 +581,10 @@ class FileAttachmentsAPI: """List file attachments with optional filtering. Args: + remote_file_id: Filter by remote file ID. + file_name: Filter by file name. entity_type: Filter by entity type (e.g., 'ENTITY_TYPE_ASSET', 'ENTITY_TYPE_RUN'). entity_id: Filter by entity ID. - query_filter: Optional CEL query filter. order_by: The field to order by. limit: Maximum number of results to return. page_size: Number of results per page. From 15b3da3ce11c825d3f86e8363c10e69a8cbc07c1 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 14:23:41 -0800 Subject: [PATCH 075/127] small fix --- python/lib/sift_client/resources/file_attachments.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index d264a41c4..0a95a4a16 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -49,6 +49,7 @@ async def get(self, *, file_attachment_id: str) -> FileAttachment: async def list_( self, *, + entity: Run | Asset | TestReport | None = None, remote_file_id: str | None = None, file_name: str | None = None, entity_type: str | None = None, @@ -71,6 +72,9 @@ async def list_( Returns: A list of FileAttachments. """ + if entity is not None: + entity_id = entity.id_ + entity_type = entity._get_entity_type_name() kwargs = {} if remote_file_id: kwargs["remote_file_id"] = remote_file_id From 1e34e7a13b273ffe887bf74251c7fae5d15faabf Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 14:26:59 -0800 Subject: [PATCH 076/127] Add entity parameter to file attachments list_ method --- python/lib/sift_client/resources/file_attachments.py | 1 + python/lib/sift_client/resources/sync_stubs/__init__.pyi | 2 ++ 2 files changed, 3 insertions(+) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 0a95a4a16..aac2d5948 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -61,6 +61,7 @@ async def list_( """List file attachments with optional filtering. Args: + entity: Filter by entity (Run, Asset, or TestReport). remote_file_id: Filter by remote file ID. file_name: Filter by file name. entity_type: Filter by entity type (e.g., 'ENTITY_TYPE_ASSET', 'ENTITY_TYPE_RUN'). diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index df5dc08c4..8e7e97959 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -570,6 +570,7 @@ class FileAttachmentsAPI: def list_( self, *, + entity: Run | Asset | TestReport | None = None, remote_file_id: str | None = None, file_name: str | None = None, entity_type: str | None = None, @@ -581,6 +582,7 @@ class FileAttachmentsAPI: """List file attachments with optional filtering. Args: + entity: Filter by entity (Run, Asset, or TestReport). remote_file_id: Filter by remote file ID. file_name: Filter by file name. entity_type: Filter by entity type (e.g., 'ENTITY_TYPE_ASSET', 'ENTITY_TYPE_RUN'). From 01866bfdb3469fff0de752505bddcc2a5bef3f9a Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 14:30:11 -0800 Subject: [PATCH 077/127] imports fix --- python/lib/sift_client/resources/file_attachments.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index aac2d5948..c2bb531a5 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -12,6 +12,9 @@ from sift_py.file_attachment.entity import Entity from sift_client.client import SiftClient + from sift_client.sift_types.asset import Asset + from sift_client.sift_types.run import Run + from sift_client.sift_types.test_report import TestReport class FileAttachmentsAPIAsync(ResourceBase): From 7dc55716499a424f2d4c4ca94f69f4d456f08a9e Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 14:52:46 -0800 Subject: [PATCH 078/127] Restore test_value.py that was accidentally deleted --- .../sift_stream_bindings/tests/test_value.py | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 rust/crates/sift_stream_bindings/tests/test_value.py diff --git a/rust/crates/sift_stream_bindings/tests/test_value.py b/rust/crates/sift_stream_bindings/tests/test_value.py new file mode 100644 index 000000000..4a670401d --- /dev/null +++ b/rust/crates/sift_stream_bindings/tests/test_value.py @@ -0,0 +1,117 @@ +import pytest +from sift_stream_bindings import ValuePy + + +class TestValuePy: + """Test ValuePy functionality.""" + + def test_create_bool_value(self): + """Test creating boolean values.""" + val_true = ValuePy.Bool(True) + val_false = ValuePy.Bool(False) + + assert val_true.is_bool() + assert val_false.is_bool() + assert val_true.as_bool() is True + assert val_false.as_bool() is False + + def test_create_string_value(self): + """Test creating string values.""" + val = ValuePy.String("hello world") + + assert val.is_string() + assert val.as_string() == "hello world" + + def test_create_float_value(self): + """Test creating float values.""" + val = ValuePy.Float(3.14) + + assert val.is_float() + assert abs(val.as_float() - 3.14) < 0.001 + + def test_create_double_value(self): + """Test creating double values.""" + val = ValuePy.Double(3.141592653589793) + + assert val.is_double() + assert abs(val.as_double() - 3.141592653589793) < 1e-10 + + def test_create_int32_value(self): + """Test creating int32 values.""" + val = ValuePy.Int32(42) + + assert val.is_int32() + assert val.as_int32() == 42 + + def test_create_uint32_value(self): + """Test creating uint32 values.""" + val = ValuePy.Uint32(42) + + assert val.is_uint32() + assert val.as_uint32() == 42 + + def test_create_int64_value(self): + """Test creating int64 values.""" + val = ValuePy.Int64(9223372036854775807) + + assert val.is_int64() + assert val.as_int64() == 9223372036854775807 + + def test_create_uint64_value(self): + """Test creating uint64 values.""" + val = ValuePy.Uint64(18446744073709551615) + + assert val.is_uint64() + assert val.as_uint64() == 18446744073709551615 + + def test_create_enum_value(self): + """Test creating enum values.""" + val = ValuePy.Enum(2) + + assert val.is_enum() + assert val.as_enum() == 2 + + def test_create_bit_field_value(self): + """Test creating bit field values.""" + # BitField expects Vec, so pass list of bytes + val = ValuePy.BitField([0x0A]) + + assert val.is_bitfield() + assert val.as_bitfield() == b'\x0A' + + def test_type_error_on_wrong_accessor(self): + """Test that accessing with wrong type raises error.""" + val = ValuePy.Bool(True) + + with pytest.raises(TypeError): + val.as_string() + + with pytest.raises(TypeError): + val.as_int32() + + def test_multiple_values(self): + """Test creating multiple values of different types.""" + values = [ + ValuePy.Bool(True), + ValuePy.String("test"), + ValuePy.Float(1.5), + ValuePy.Double(2.5), + ValuePy.Int32(-100), + ValuePy.Uint32(100), + ValuePy.Int64(-1000000), + ValuePy.Uint64(1000000), + ValuePy.Enum(5), + ValuePy.BitField([0xFF]), + ] + + assert len(values) == 10 + assert values[0].is_bool() + assert values[1].is_string() + assert values[2].is_float() + assert values[3].is_double() + assert values[4].is_int32() + assert values[5].is_uint32() + assert values[6].is_int64() + assert values[7].is_uint64() + assert values[8].is_enum() + assert values[9].is_bitfield() From 164a013de4441593052433b2304961892c8ef692 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 14:54:46 -0800 Subject: [PATCH 079/127] Restore sift_stream_bindings files to match main --- .../sift_stream_bindings.pyi | 338 +++++++++++------- .../sift_stream_bindings/tests/test_stream.py | 27 +- 2 files changed, 217 insertions(+), 148 deletions(-) diff --git a/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi b/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi index 788bc20cd..8aab9dcaa 100644 --- a/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi +++ b/rust/crates/sift_stream_bindings/sift_stream_bindings.pyi @@ -1,38 +1,59 @@ # This file is automatically generated by pyo3_stub_gen __all__ = [ + "BackupMetricsSnapshotPy", "ChannelBitFieldElementPy", "ChannelConfigPy", "ChannelDataTypePy", + "ChannelEnumPy", "ChannelEnumTypePy", "ChannelValuePy", "ChannelValueTypePy", + "CheckpointMetricsSnapshotPy", + "DiskBackupPolicyPy", "DurationPy", "FlowConfigPy", "FlowPy", "IngestWithConfigDataChannelValuePy", "IngestWithConfigDataStreamRequestPy", "IngestionConfigFormPy", + "MetadataPy", + "MetadataValuePy", "RecoveryStrategyPy", "RetryPolicyPy", + "RollingFilePolicyPy", "RunFormPy", + "RunSelectorPy", "SiftStreamBuilderPy", + "SiftStreamMetricsSnapshotPy", "SiftStreamPy", "TimeValuePy", + "ValuePy", ] import builtins import typing from enum import Enum +@typing.final +class BackupMetricsSnapshotPy: + cur_checkpoint_file_count: builtins.int + cur_checkpoint_cur_file_size: builtins.int + cur_checkpoint_bytes: builtins.int + cur_checkpoint_messages: builtins.int + total_file_count: builtins.int + total_bytes: builtins.int + total_messages: builtins.int + files_pending_ingestion: builtins.int + files_ingested: builtins.int + cur_ingest_retries: builtins.int + @typing.final class ChannelBitFieldElementPy: name: builtins.str index: builtins.int bit_count: builtins.int - def __new__( - cls, name: builtins.str, index: builtins.int, bit_count: builtins.int - ) -> ChannelBitFieldElementPy: ... + def __new__(cls, name:builtins.str, index:builtins.int, bit_count:builtins.int) -> ChannelBitFieldElementPy: ... @typing.final class ChannelConfigPy: @@ -42,121 +63,112 @@ class ChannelConfigPy: data_type: ChannelDataTypePy enum_types: builtins.list[ChannelEnumTypePy] bit_field_elements: builtins.list[ChannelBitFieldElementPy] - def __new__( - cls, - name: builtins.str, - unit: builtins.str, - description: builtins.str, - data_type: ChannelDataTypePy, - enum_types: typing.Sequence[ChannelEnumTypePy], - bit_field_elements: typing.Sequence[ChannelBitFieldElementPy], - ) -> ChannelConfigPy: ... + def __new__(cls, name:builtins.str, unit:builtins.str, description:builtins.str, data_type:ChannelDataTypePy, enum_types:typing.Sequence[ChannelEnumTypePy], bit_field_elements:typing.Sequence[ChannelBitFieldElementPy]) -> ChannelConfigPy: ... + +@typing.final +class ChannelEnumPy: + def __new__(cls, val:builtins.int) -> ChannelEnumPy: ... @typing.final class ChannelEnumTypePy: name: builtins.str key: builtins.int - def __new__(cls, name: builtins.str, key: builtins.int) -> ChannelEnumTypePy: ... + def __new__(cls, name:builtins.str, key:builtins.int) -> ChannelEnumTypePy: ... @typing.final class ChannelValuePy: - @staticmethod - def bool(name: builtins.str, value: builtins.bool) -> ChannelValuePy: ... - @staticmethod - def string(name: builtins.str, value: builtins.str) -> ChannelValuePy: ... - @staticmethod - def float(name: builtins.str, value: builtins.float) -> ChannelValuePy: ... - @staticmethod - def double(name: builtins.str, value: builtins.float) -> ChannelValuePy: ... - @staticmethod - def int32(name: builtins.str, value: builtins.int) -> ChannelValuePy: ... - @staticmethod - def uint32(name: builtins.str, value: builtins.int) -> ChannelValuePy: ... - @staticmethod - def int64(name: builtins.str, value: builtins.int) -> ChannelValuePy: ... - @staticmethod - def uint64(name: builtins.str, value: builtins.int) -> ChannelValuePy: ... - @staticmethod - def enum_value(name: builtins.str, value: ChannelEnumTypePy) -> ChannelValuePy: ... - @staticmethod - def bitfield( - name: builtins.str, value: typing.Sequence[builtins.int] - ) -> ChannelValuePy: ... + name: builtins.str + value: ValuePy + def __new__(cls, name:builtins.str, value:ValuePy) -> ChannelValuePy: ... @typing.final class ChannelValueTypePy: @staticmethod - def bool(value: builtins.bool) -> ChannelValueTypePy: ... + def bool(value:builtins.bool) -> ChannelValueTypePy: ... @staticmethod - def string(value: builtins.str) -> ChannelValueTypePy: ... + def string(value:builtins.str) -> ChannelValueTypePy: ... @staticmethod - def float(value: builtins.float) -> ChannelValueTypePy: ... + def float(value:builtins.float) -> ChannelValueTypePy: ... @staticmethod - def double(value: builtins.float) -> ChannelValueTypePy: ... + def double(value:builtins.float) -> ChannelValueTypePy: ... @staticmethod - def int32(value: builtins.int) -> ChannelValueTypePy: ... + def int32(value:builtins.int) -> ChannelValueTypePy: ... @staticmethod - def uint32(value: builtins.int) -> ChannelValueTypePy: ... + def uint32(value:builtins.int) -> ChannelValueTypePy: ... @staticmethod - def int64(value: builtins.int) -> ChannelValueTypePy: ... + def int64(value:builtins.int) -> ChannelValueTypePy: ... @staticmethod - def uint64(value: builtins.int) -> ChannelValueTypePy: ... + def uint64(value:builtins.int) -> ChannelValueTypePy: ... @staticmethod - def enum_value(value: builtins.int) -> ChannelValueTypePy: ... + def enum_value(value:builtins.int) -> ChannelValueTypePy: ... @staticmethod - def bitfield(value: typing.Sequence[builtins.int]) -> ChannelValueTypePy: ... + def bitfield(value:typing.Sequence[builtins.int]) -> ChannelValueTypePy: ... @staticmethod - def bytes(value: typing.Sequence[builtins.int]) -> ChannelValueTypePy: ... + def bytes(value:typing.Sequence[builtins.int]) -> ChannelValueTypePy: ... @staticmethod def empty() -> ChannelValueTypePy: ... +@typing.final +class CheckpointMetricsSnapshotPy: + checkpoint_count: builtins.int + failed_checkpoint_count: builtins.int + checkpoint_timer_reached_cnt: builtins.int + checkpoint_manually_reached_cnt: builtins.int + cur_elapsed_secs: builtins.float + cur_messages_sent: builtins.int + cur_message_rate: builtins.float + cur_bytes_sent: builtins.int + cur_byte_rate: builtins.float + +@typing.final +class DiskBackupPolicyPy: + backups_dir: typing.Optional[builtins.str] + max_backup_file_size: builtins.int + rolling_file_policy: RollingFilePolicyPy + retain_backups: builtins.bool + def __new__(cls, backups_dir:typing.Optional[builtins.str], max_backup_file_size:builtins.int, rolling_file_policy:RollingFilePolicyPy, retain_backups:builtins.bool) -> DiskBackupPolicyPy: ... + @staticmethod + def default() -> DiskBackupPolicyPy: ... + @typing.final class DurationPy: secs: builtins.int nanos: builtins.int - def __new__(cls, secs: builtins.int, nanos: builtins.int) -> DurationPy: ... + def __new__(cls, secs:builtins.int, nanos:builtins.int) -> DurationPy: ... @typing.final class FlowConfigPy: name: builtins.str channels: builtins.list[ChannelConfigPy] - def __new__( - cls, name: builtins.str, channels: typing.Sequence[ChannelConfigPy] - ) -> FlowConfigPy: ... + def __new__(cls, name:builtins.str, channels:typing.Sequence[ChannelConfigPy]) -> FlowConfigPy: ... @typing.final class FlowPy: - def __new__( - cls, - flow_name: builtins.str, - timestamp: TimeValuePy, - values: typing.Sequence[ChannelValuePy], - ) -> FlowPy: ... + def __new__(cls, flow_name:builtins.str, timestamp:TimeValuePy, values:typing.Sequence[ChannelValuePy]) -> FlowPy: ... @typing.final class IngestWithConfigDataChannelValuePy: + ty: ChannelValueTypePy @staticmethod - def bool(value: builtins.bool) -> IngestWithConfigDataChannelValuePy: ... + def bool(value:builtins.bool) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def string(value: builtins.str) -> IngestWithConfigDataChannelValuePy: ... + def string(value:builtins.str) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def float(value: builtins.float) -> IngestWithConfigDataChannelValuePy: ... + def float(value:builtins.float) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def double(value: builtins.float) -> IngestWithConfigDataChannelValuePy: ... + def double(value:builtins.float) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def int32(value: builtins.int) -> IngestWithConfigDataChannelValuePy: ... + def int32(value:builtins.int) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def uint32(value: builtins.int) -> IngestWithConfigDataChannelValuePy: ... + def uint32(value:builtins.int) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def int64(value: builtins.int) -> IngestWithConfigDataChannelValuePy: ... + def int64(value:builtins.int) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def uint64(value: builtins.int) -> IngestWithConfigDataChannelValuePy: ... + def uint64(value:builtins.int) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def enum_value(value: builtins.int) -> IngestWithConfigDataChannelValuePy: ... + def enum_value(value:builtins.int) -> IngestWithConfigDataChannelValuePy: ... @staticmethod - def bitfield( - value: typing.Sequence[builtins.int], - ) -> IngestWithConfigDataChannelValuePy: ... + def bitfield(value:typing.Sequence[builtins.int]) -> IngestWithConfigDataChannelValuePy: ... @staticmethod def empty() -> IngestWithConfigDataChannelValuePy: ... @@ -169,62 +181,29 @@ class IngestWithConfigDataStreamRequestPy: run_id: builtins.str end_stream_on_validation_error: builtins.bool organization_id: builtins.str - def __new__( - cls, - ingestion_config_id: builtins.str, - flow: builtins.str, - timestamp: typing.Optional[TimeValuePy], - channel_values: typing.Sequence[IngestWithConfigDataChannelValuePy], - run_id: builtins.str, - end_stream_on_validation_error: builtins.bool, - organization_id: builtins.str, - ) -> IngestWithConfigDataStreamRequestPy: ... + def __new__(cls, ingestion_config_id:builtins.str, flow:builtins.str, timestamp:typing.Optional[TimeValuePy], channel_values:typing.Sequence[IngestWithConfigDataChannelValuePy], run_id:builtins.str, end_stream_on_validation_error:builtins.bool, organization_id:builtins.str) -> IngestWithConfigDataStreamRequestPy: ... @typing.final class IngestionConfigFormPy: asset_name: builtins.str flows: builtins.list[FlowConfigPy] client_key: builtins.str - def __new__( - cls, - asset_name: builtins.str, - client_key: builtins.str, - flows: typing.Sequence[FlowConfigPy], - ) -> IngestionConfigFormPy: ... + def __new__(cls, asset_name:builtins.str, client_key:builtins.str, flows:typing.Sequence[FlowConfigPy]) -> IngestionConfigFormPy: ... + +@typing.final +class MetadataPy: + key: builtins.str + value: MetadataValuePy + def __new__(cls, key:builtins.str, value:MetadataValuePy) -> MetadataPy: ... @typing.final class RecoveryStrategyPy: - strategy_type: builtins.str - retry_policy: typing.Optional[RetryPolicyPy] - max_buffer_size: typing.Optional[builtins.int] - backups_dir: typing.Optional[builtins.str] - max_backups_file_size: typing.Optional[builtins.int] - def __new__( - cls, - strategy_type: builtins.str, - retry_policy: typing.Optional[RetryPolicyPy], - max_buffer_size: typing.Optional[builtins.int], - backups_dir: typing.Optional[builtins.str], - max_backups_file_size: typing.Optional[builtins.int], - ) -> RecoveryStrategyPy: ... - @staticmethod - def retry_only(retry_policy: RetryPolicyPy) -> RecoveryStrategyPy: ... - @staticmethod - def retry_with_in_memory_backups( - retry_policy: RetryPolicyPy, max_buffer_size: typing.Optional[builtins.int] - ) -> RecoveryStrategyPy: ... - @staticmethod - def retry_with_disk_backups( - retry_policy: RetryPolicyPy, - backups_dir: typing.Optional[builtins.str], - max_backups_file_size: typing.Optional[builtins.int], - ) -> RecoveryStrategyPy: ... @staticmethod - def default() -> RecoveryStrategyPy: ... + def retry_only(retry_policy:RetryPolicyPy) -> RecoveryStrategyPy: ... @staticmethod - def default_retry_policy_in_memory_backups() -> RecoveryStrategyPy: ... + def retry_with_backups(retry_policy:RetryPolicyPy, disk_backup_policy:DiskBackupPolicyPy) -> RecoveryStrategyPy: ... @staticmethod - def default_retry_policy_disk_backups() -> RecoveryStrategyPy: ... + def default() -> RecoveryStrategyPy: ... @typing.final class RetryPolicyPy: @@ -232,29 +211,31 @@ class RetryPolicyPy: initial_backoff: DurationPy max_backoff: DurationPy backoff_multiplier: builtins.int - def __new__( - cls, - max_attempts: builtins.int, - initial_backoff: DurationPy, - max_backoff: DurationPy, - backoff_multiplier: builtins.int, - ) -> RetryPolicyPy: ... + def __new__(cls, max_attempts:builtins.int, initial_backoff:DurationPy, max_backoff:DurationPy, backoff_multiplier:builtins.int) -> RetryPolicyPy: ... @staticmethod def default() -> RetryPolicyPy: ... +@typing.final +class RollingFilePolicyPy: + def __new__(cls, max_file_count:typing.Optional[builtins.int]) -> RollingFilePolicyPy: ... + @staticmethod + def default() -> RollingFilePolicyPy: ... + @typing.final class RunFormPy: name: builtins.str client_key: builtins.str description: typing.Optional[builtins.str] tags: typing.Optional[builtins.list[builtins.str]] - def __new__( - cls, - name: builtins.str, - client_key: builtins.str, - description: typing.Optional[builtins.str], - tags: typing.Optional[typing.Sequence[builtins.str]], - ) -> RunFormPy: ... + metadata: typing.Optional[builtins.list[MetadataPy]] + def __new__(cls, name:builtins.str, client_key:builtins.str, description:typing.Optional[builtins.str], tags:typing.Optional[typing.Sequence[builtins.str]], metadata:typing.Optional[typing.Sequence[MetadataPy]]) -> RunFormPy: ... + +@typing.final +class RunSelectorPy: + @staticmethod + def by_id(run_id:builtins.str) -> RunSelectorPy: ... + @staticmethod + def by_form(form:RunFormPy) -> RunSelectorPy: ... @typing.final class SiftStreamBuilderPy: @@ -263,35 +244,98 @@ class SiftStreamBuilderPy: enable_tls: builtins.bool ingestion_config: typing.Optional[IngestionConfigFormPy] recovery_strategy: typing.Optional[RecoveryStrategyPy] - checkpoint_interval: DurationPy + checkpoint_interval: typing.Optional[DurationPy] run: typing.Optional[RunFormPy] run_id: typing.Optional[builtins.str] - def __new__( - cls, uri: builtins.str, apikey: builtins.str - ) -> SiftStreamBuilderPy: ... + asset_tags: typing.Optional[builtins.list[builtins.str]] + metadata: typing.Optional[builtins.list[MetadataPy]] + def __new__(cls, uri:builtins.str, apikey:builtins.str) -> SiftStreamBuilderPy: ... def build(self) -> typing.Any: ... +@typing.final +class SiftStreamMetricsSnapshotPy: + elapsed_secs: builtins.float + loaded_flows: builtins.int + unique_flows_received: builtins.int + messages_received: builtins.int + messages_sent: builtins.int + message_rate: builtins.float + bytes_sent: builtins.int + byte_rate: builtins.float + messages_sent_to_backup: builtins.int + cur_retry_count: builtins.int + checkpoint: CheckpointMetricsSnapshotPy + backups: BackupMetricsSnapshotPy + @typing.final class SiftStreamPy: - def send(self, flow: FlowPy) -> typing.Any: ... - def send_requests( - self, requests: typing.Sequence[IngestWithConfigDataStreamRequestPy] - ) -> typing.Any: ... + def send(self, flow:FlowPy) -> typing.Any: ... + def batch_send(self, flows:typing.Any) -> typing.Any: ... + def send_requests(self, requests:typing.Sequence[IngestWithConfigDataStreamRequestPy]) -> typing.Any: ... + def get_metrics_snapshot(self) -> SiftStreamMetricsSnapshotPy: ... + def add_new_flows(self, flow_configs:typing.Sequence[FlowConfigPy]) -> typing.Any: ... + def get_flows(self) -> builtins.dict[builtins.str, FlowConfigPy]: ... + def attach_run(self, run_selector:RunSelectorPy) -> typing.Any: ... + def detach_run(self) -> None: ... + def run(self) -> typing.Optional[builtins.str]: ... def finish(self) -> typing.Any: ... @typing.final class TimeValuePy: def __new__(cls) -> TimeValuePy: ... @staticmethod - def from_timestamp(secs: builtins.int, nsecs: builtins.int) -> TimeValuePy: ... + def from_timestamp(secs:builtins.int, nsecs:builtins.int) -> TimeValuePy: ... @staticmethod - def from_timestamp_millis(millis: builtins.int) -> TimeValuePy: ... + def from_timestamp_millis(millis:builtins.int) -> TimeValuePy: ... @staticmethod - def from_timestamp_micros(micros: builtins.int) -> TimeValuePy: ... + def from_timestamp_micros(micros:builtins.int) -> TimeValuePy: ... @staticmethod - def from_timestamp_nanos(nanos: builtins.int) -> TimeValuePy: ... + def from_timestamp_nanos(nanos:builtins.int) -> TimeValuePy: ... @staticmethod - def from_rfc3339(val: builtins.str) -> TimeValuePy: ... + def from_rfc3339(val:builtins.str) -> TimeValuePy: ... + +@typing.final +class ValuePy: + @staticmethod + def Bool(value:builtins.bool) -> ValuePy: ... + @staticmethod + def String(value:builtins.str) -> ValuePy: ... + @staticmethod + def Float(value:builtins.float) -> ValuePy: ... + @staticmethod + def Double(value:builtins.float) -> ValuePy: ... + @staticmethod + def Int32(value:builtins.int) -> ValuePy: ... + @staticmethod + def Int64(value:builtins.int) -> ValuePy: ... + @staticmethod + def Uint32(value:builtins.int) -> ValuePy: ... + @staticmethod + def Uint64(value:builtins.int) -> ValuePy: ... + @staticmethod + def Enum(value:builtins.int) -> ValuePy: ... + @staticmethod + def BitField(value:typing.Sequence[builtins.int]) -> ValuePy: ... + def is_bool(self) -> builtins.bool: ... + def is_string(self) -> builtins.bool: ... + def is_float(self) -> builtins.bool: ... + def is_double(self) -> builtins.bool: ... + def is_int32(self) -> builtins.bool: ... + def is_int64(self) -> builtins.bool: ... + def is_uint32(self) -> builtins.bool: ... + def is_uint64(self) -> builtins.bool: ... + def is_enum(self) -> builtins.bool: ... + def is_bitfield(self) -> builtins.bool: ... + def as_bool(self) -> builtins.bool: ... + def as_string(self) -> builtins.str: ... + def as_float(self) -> builtins.float: ... + def as_double(self) -> builtins.float: ... + def as_int32(self) -> builtins.int: ... + def as_int64(self) -> builtins.int: ... + def as_uint32(self) -> builtins.int: ... + def as_uint64(self) -> builtins.int: ... + def as_enum(self) -> builtins.int: ... + def as_bitfield(self) -> builtins.list[builtins.int]: ... @typing.final class ChannelDataTypePy(Enum): @@ -307,3 +351,19 @@ class ChannelDataTypePy(Enum): Int64 = ... Uint64 = ... Bytes = ... + +@typing.final +class MetadataValuePy(Enum): + String = ... + Number = ... + Boolean = ... + + def __new__(cls, obj:typing.Any) -> MetadataValuePy: ... + + def __str__(self) -> builtins.str: ... + + def is_string(self) -> builtins.bool: ... + + def is_number(self) -> builtins.bool: ... + + def is_boolean(self) -> builtins.bool: ... diff --git a/rust/crates/sift_stream_bindings/tests/test_stream.py b/rust/crates/sift_stream_bindings/tests/test_stream.py index 648b30499..f3ea81c51 100644 --- a/rust/crates/sift_stream_bindings/tests/test_stream.py +++ b/rust/crates/sift_stream_bindings/tests/test_stream.py @@ -4,14 +4,14 @@ from sift_stream_bindings import ( ChannelConfigPy, ChannelDataTypePy, - ChannelValuePy, FlowConfigPy, FlowPy, IngestionConfigFormPy, - IngestWithConfigDataStreamRequestPy, RunFormPy, SiftStreamBuilderPy, TimeValuePy, + ValuePy, + IngestWithConfigDataStreamRequestPy ) @@ -26,12 +26,13 @@ def test_create_empty_flow(self): def test_create_flow_with_multiple_values(self): """Test creating a flow with multiple channel values.""" + from sift_stream_bindings import ChannelValuePy timestamp = TimeValuePy.from_timestamp(int(time.time()), 0) values = [ - ChannelValuePy.float("temperature", 23.5), - ChannelValuePy.bool("active", True), - ChannelValuePy.string("status", "running"), - ChannelValuePy.int32("count", 42), + ChannelValuePy("temperature", ValuePy.Float(23.5)), + ChannelValuePy("active", ValuePy.Bool(True)), + ChannelValuePy("status", ValuePy.String("running")), + ChannelValuePy("count", ValuePy.Int32(42)), ] flow = FlowPy("test_flow", timestamp, values) assert flow @@ -49,8 +50,7 @@ def test_create_stream_builder(self): assert builder.enable_tls is True assert builder.ingestion_config is None assert builder.recovery_strategy is None - assert builder.checkpoint_interval.secs == 60 - assert builder.checkpoint_interval.nanos == 0 + assert builder.checkpoint_interval is None def test_set_ingestion_config(self): """Test setting ingestion config on builder.""" @@ -87,13 +87,20 @@ def test_set_ingestion_config(self): def test_set_run_form(self): """Test setting run form on builder.""" + from sift_stream_bindings import MetadataPy, MetadataValuePy + builder = SiftStreamBuilderPy("https://api.example.com", "test-api-key") + metadata = [ + MetadataPy(key="test_key", value=MetadataValuePy("test_value")), + ] + run_form = RunFormPy( name="Test Run", - description="Test run description", client_key="test-run-key", + description="Test run description", tags=[], + metadata=metadata, ) builder.run = run_form @@ -102,6 +109,8 @@ def test_set_run_form(self): assert builder.run.description == "Test run description" assert builder.run.client_key == "test-run-key" assert builder.run.tags == [] + assert builder.run.metadata is not None + assert len(builder.run.metadata) == 1 @pytest.mark.asyncio async def test_build_stream_no_ingestion_config(self): From 7896a2d435a357afb3568d50f1665a9c3c727acf Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 14:59:46 -0800 Subject: [PATCH 080/127] don't use Entity --- python/lib/sift_client/resources/file_attachments.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index c2bb531a5..1094ddd0b 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -9,8 +9,6 @@ from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate if TYPE_CHECKING: - from sift_py.file_attachment.entity import Entity - from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset from sift_client.sift_types.run import Run @@ -185,7 +183,7 @@ async def upload( self, *, path: str | Path, - entity: Entity, + entity: Asset | Run | TestReport, metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, @@ -205,8 +203,8 @@ async def upload( """ remote_file_id = await self._upload_client.upload_attachment( path=path, - entity_id=entity.entity_id, - entity_type=entity.entity_type.value, + entity_id=entity._id_or_error, + entity_type=entity._get_entity_type_name(), metadata=metadata, description=description, organization_id=organization_id, From a26b426c516e63956e68e6b3f99c747054498e87 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 15:29:12 -0800 Subject: [PATCH 081/127] add tests and use cel filter --- .../low_level_wrappers/remote_files.py | 4 +- .../_tests/resources/test_file_attachments.py | 561 ++++++++++++++++++ .../sift_client/resources/file_attachments.py | 27 +- .../resources/sync_stubs/__init__.pyi | 3 +- .../sift_client/sift_types/file_attachment.py | 4 + 5 files changed, 586 insertions(+), 13 deletions(-) create mode 100644 python/lib/sift_client/_tests/resources/test_file_attachments.py diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 345b10506..3412213f6 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -60,7 +60,7 @@ async def get_remote_file( async def list_all_remote_files( self, - kwargs: dict[str, Any] | None = None, + query_filter: str | None = None, order_by: str | None = None, max_results: int | None = None, page_size: int | None = None, @@ -80,7 +80,7 @@ async def list_all_remote_files( """ return await self._handle_pagination( self.list_remote_files, - kwargs=kwargs, + kwargs={"query_filter": query_filter, "sift_client": sift_client}, page_size=page_size, order_by=order_by, max_results=max_results, diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py new file mode 100644 index 000000000..97577fe08 --- /dev/null +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -0,0 +1,561 @@ +"""Pytest tests for the File Attachments API. + +These tests demonstrate and validate the usage of the File Attachments API including: +- Basic file attachment operations (get, list, upload, download) +- File attachment filtering by entity +- File attachment updates and deletion +- Error handling and edge cases +""" + +import os +import tempfile +from pathlib import Path + +import pytest + +from sift_client import SiftClient +from sift_client.resources import FileAttachmentsAPI, FileAttachmentsAPIAsync +from sift_client.sift_types import FileAttachment, FileAttachmentUpdate + +pytestmark = pytest.mark.integration + + +def test_client_binding(sift_client): + """Test that file_attachments API is properly bound to the client.""" + assert sift_client.file_attachments + assert isinstance(sift_client.file_attachments, FileAttachmentsAPI) + assert sift_client.async_.file_attachments + assert isinstance(sift_client.async_.file_attachments, FileAttachmentsAPIAsync) + + +@pytest.fixture +def file_attachments_api_async(sift_client: SiftClient): + """Get the async file attachments API instance.""" + return sift_client.async_.file_attachments + + +@pytest.fixture +def file_attachments_api_sync(sift_client: SiftClient): + """Get the synchronous file attachments API instance.""" + return sift_client.file_attachments + + +@pytest.fixture +def test_run(sift_client: SiftClient): + """Get a test run to attach files to.""" + runs = sift_client.runs.list_(limit=1) + if runs: + return runs[0] + pytest.skip("No runs available for testing") + + +@pytest.fixture +def test_asset(sift_client: SiftClient): + """Get a test asset to attach files to.""" + assets = sift_client.assets.list_(limit=1) + if assets: + return assets[0] + pytest.skip("No assets available for testing") + + +@pytest.fixture +async def uploaded_file_attachment(file_attachments_api_async, test_run): + """Upload a test file and return the file attachment, cleaning up after test.""" + # Create a temporary test file + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: + tmp.write("Test file content for integration tests\n") + tmp.write("This file should be deleted after the test\n") + tmp_path = tmp.name + + try: + # Upload the file + file_attachment = await file_attachments_api_async.upload( + path=tmp_path, + entity=test_run, + description="Integration test file attachment", + ) + yield file_attachment + + # Cleanup: delete the uploaded file + try: + await file_attachments_api_async.delete(file_attachments=file_attachment) + except Exception: + pass # If deletion fails, it's okay for test cleanup + finally: + # Cleanup: delete the temporary local file + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + +class TestFileAttachmentsAPIAsync: + """Test suite for the async File Attachments API functionality.""" + + class TestUpload: + """Tests for the async upload method.""" + + @pytest.mark.asyncio + async def test_upload_to_run(self, file_attachments_api_async, test_run): + """Test uploading a file attachment to a run.""" + # Create a temporary test file + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: + tmp.write("Test file content\n") + tmp_path = tmp.name + + try: + # Upload the file + file_attachment = await file_attachments_api_async.upload( + path=tmp_path, + entity=test_run, + description="Test upload to run", + ) + + # Verify the upload + assert isinstance(file_attachment, FileAttachment) + assert file_attachment.id_ is not None + assert file_attachment.file_name is not None + assert file_attachment.description == "Test upload to run" + assert file_attachment.entity_id == test_run.id_ + + # Cleanup: delete the uploaded file + await file_attachments_api_async.delete(file_attachments=file_attachment) + finally: + # Cleanup: delete the temporary local file + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + @pytest.mark.asyncio + async def test_upload_to_asset(self, file_attachments_api_async, test_asset): + """Test uploading a file attachment to an asset.""" + # Create a temporary test file + with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as tmp: + tmp.write("col1,col2,col3\n") + tmp.write("1,2,3\n") + tmp_path = tmp.name + + try: + # Upload the file + file_attachment = await file_attachments_api_async.upload( + path=tmp_path, + entity=test_asset, + description="Test CSV upload to asset", + metadata={"test_key": "test_value"}, + ) + + # Verify the upload + assert isinstance(file_attachment, FileAttachment) + assert file_attachment.id_ is not None + assert file_attachment.entity_id == test_asset.id_ + assert file_attachment.description == "Test CSV upload to asset" + + # Cleanup + await file_attachments_api_async.delete(file_attachments=file_attachment) + finally: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + @pytest.mark.asyncio + async def test_upload_with_pathlib(self, file_attachments_api_async, test_run): + """Test uploading using pathlib.Path instead of string.""" + # Create a temporary test file + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: + tmp.write("Test pathlib upload\n") + tmp_path = Path(tmp.name) + + try: + # Upload using Path object + file_attachment = await file_attachments_api_async.upload( + path=tmp_path, + entity=test_run, + description="Test pathlib upload", + ) + + assert isinstance(file_attachment, FileAttachment) + assert file_attachment.id_ is not None + + # Cleanup + await file_attachments_api_async.delete(file_attachments=file_attachment) + finally: + if tmp_path.exists(): + tmp_path.unlink() + + class TestGet: + """Tests for the async get method.""" + + @pytest.mark.asyncio + async def test_get_by_id(self, file_attachments_api_async, uploaded_file_attachment): + """Test getting a file attachment by ID.""" + retrieved = await file_attachments_api_async.get( + file_attachment_id=uploaded_file_attachment.id_ + ) + + assert retrieved is not None + assert retrieved.id_ == uploaded_file_attachment.id_ + assert retrieved.file_name == uploaded_file_attachment.file_name + + @pytest.mark.asyncio + async def test_get_nonexistent_raises_error(self, file_attachments_api_async): + """Test that getting a non-existent file attachment raises an error.""" + with pytest.raises(Exception): # Could be more specific based on actual error + await file_attachments_api_async.get( + file_attachment_id="nonexistent-file-id-12345" + ) + + class TestList: + """Tests for the async list_ method.""" + + @pytest.mark.asyncio + async def test_basic_list(self, file_attachments_api_async): + """Test basic file attachment listing functionality.""" + file_attachments = await file_attachments_api_async.list_(limit=5) + + # Verify we get a list + assert isinstance(file_attachments, list) + + # If we have file attachments, verify their structure + if file_attachments: + fa = file_attachments[0] + assert isinstance(fa, FileAttachment) + assert fa.id_ is not None + + @pytest.mark.asyncio + async def test_list_by_entity( + self, file_attachments_api_async, uploaded_file_attachment, test_run + ): + """Test listing file attachments filtered by entity.""" + file_attachments = await file_attachments_api_async.list_( + entity=test_run, + limit=10, + ) + + assert isinstance(file_attachments, list) + + # Should find our uploaded file + found = any(fa.id_ == uploaded_file_attachment.id_ for fa in file_attachments) + assert found, "Uploaded file attachment not found in entity list" + + # All returned attachments should belong to the test run + for fa in file_attachments: + assert fa.entity_id == test_run.id_ + + @pytest.mark.asyncio + async def test_list_by_entity_id( + self, file_attachments_api_async, uploaded_file_attachment, test_run + ): + """Test listing file attachments filtered by entity_id.""" + file_attachments = await file_attachments_api_async.list_( + entity_id=test_run.id_, + limit=10, + ) + + assert isinstance(file_attachments, list) + + # Should find our uploaded file + found = any(fa.id_ == uploaded_file_attachment.id_ for fa in file_attachments) + assert found + + @pytest.mark.asyncio + async def test_list_by_file_name( + self, file_attachments_api_async, uploaded_file_attachment + ): + """Test listing file attachments filtered by file name.""" + file_attachments = await file_attachments_api_async.list_( + file_name=uploaded_file_attachment.file_name, + ) + + assert isinstance(file_attachments, list) + + # Should find at least our uploaded file + found = any(fa.id_ == uploaded_file_attachment.id_ for fa in file_attachments) + assert found + + # All returned attachments should have the specified file name + for fa in file_attachments: + assert fa.file_name == uploaded_file_attachment.file_name + + @pytest.mark.asyncio + async def test_list_with_limit(self, file_attachments_api_async): + """Test file attachment listing with different limits.""" + # Test with limit of 1 + fas_1 = await file_attachments_api_async.list_(limit=1) + assert isinstance(fas_1, list) + assert len(fas_1) <= 1 + + # Test with limit of 3 + fas_3 = await file_attachments_api_async.list_(limit=3) + assert isinstance(fas_3, list) + assert len(fas_3) <= 3 + + class TestUpdate: + """Tests for the async update method.""" + + @pytest.mark.asyncio + async def test_update_description( + self, file_attachments_api_async, uploaded_file_attachment + ): + """Test updating a file attachment's description.""" + new_description = "Updated description for integration test" + + update = FileAttachmentUpdate( + id_=uploaded_file_attachment.id_, + description=new_description, + ) + + updated = await file_attachments_api_async.update(file_attachment=update) + + assert updated.id_ == uploaded_file_attachment.id_ + assert updated.description == new_description + + @pytest.mark.asyncio + async def test_update_with_dict( + self, file_attachments_api_async, uploaded_file_attachment + ): + """Test updating a file attachment using a dict.""" + new_description = "Updated via dict" + + updated = await file_attachments_api_async.update( + file_attachment={ + "id_": uploaded_file_attachment.id_, + "description": new_description, + } + ) + + assert updated.id_ == uploaded_file_attachment.id_ + assert updated.description == new_description + + class TestDelete: + """Tests for the async delete method.""" + + @pytest.mark.asyncio + async def test_delete_single_by_id(self, file_attachments_api_async, test_run): + """Test deleting a single file attachment by ID string.""" + # Upload a file to delete + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: + tmp.write("File to delete\n") + tmp_path = tmp.name + + try: + file_attachment = await file_attachments_api_async.upload( + path=tmp_path, + entity=test_run, + description="File to delete", + ) + + # Delete by ID string + await file_attachments_api_async.delete(file_attachments=file_attachment.id_) + + # Verify it's deleted + with pytest.raises(Exception): + await file_attachments_api_async.get(file_attachment_id=file_attachment.id_) + finally: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + @pytest.mark.asyncio + async def test_delete_single_by_object(self, file_attachments_api_async, test_run): + """Test deleting a single file attachment by FileAttachment object.""" + # Upload a file to delete + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: + tmp.write("File to delete\n") + tmp_path = tmp.name + + try: + file_attachment = await file_attachments_api_async.upload( + path=tmp_path, + entity=test_run, + description="File to delete by object", + ) + + # Delete by FileAttachment object + await file_attachments_api_async.delete(file_attachments=file_attachment) + + # Verify it's deleted + with pytest.raises(Exception): + await file_attachments_api_async.get(file_attachment_id=file_attachment.id_) + finally: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + @pytest.mark.asyncio + async def test_delete_multiple(self, file_attachments_api_async, test_run): + """Test deleting multiple file attachments at once.""" + # Upload multiple files + file_attachments = [] + tmp_paths = [] + + try: + for i in range(3): + with tempfile.NamedTemporaryFile( + mode="w", suffix=".txt", delete=False + ) as tmp: + tmp.write(f"File {i} to delete\n") + tmp_paths.append(tmp.name) + + fa = await file_attachments_api_async.upload( + path=tmp_paths[-1], + entity=test_run, + description=f"File {i} to delete", + ) + file_attachments.append(fa) + + # Delete all at once + await file_attachments_api_async.delete(file_attachments=file_attachments) + + # Verify they're all deleted + for fa in file_attachments: + with pytest.raises(Exception): + await file_attachments_api_async.get(file_attachment_id=fa.id_) + finally: + for tmp_path in tmp_paths: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + @pytest.mark.asyncio + async def test_delete_list_of_ids(self, file_attachments_api_async, test_run): + """Test deleting multiple file attachments using a list of ID strings.""" + # Upload multiple files + file_attachments = [] + tmp_paths = [] + + try: + for i in range(2): + with tempfile.NamedTemporaryFile( + mode="w", suffix=".txt", delete=False + ) as tmp: + tmp.write(f"File {i} to delete\n") + tmp_paths.append(tmp.name) + + fa = await file_attachments_api_async.upload( + path=tmp_paths[-1], + entity=test_run, + description=f"File {i} to delete by ID", + ) + file_attachments.append(fa) + + # Delete using list of IDs + ids = [fa.id_ for fa in file_attachments] + await file_attachments_api_async.delete(file_attachments=ids) + + # Verify they're all deleted + for fa_id in ids: + with pytest.raises(Exception): + await file_attachments_api_async.get(file_attachment_id=fa_id) + finally: + for tmp_path in tmp_paths: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + class TestDownload: + """Tests for the async download methods.""" + + @pytest.mark.asyncio + async def test_get_download_url( + self, file_attachments_api_async, uploaded_file_attachment + ): + """Test getting a download URL for a file attachment.""" + url = await file_attachments_api_async.get_download_url( + file_attachment=uploaded_file_attachment + ) + + assert isinstance(url, str) + assert len(url) > 0 + # URL should be a valid HTTP/HTTPS URL + assert url.startswith("http://") or url.startswith("https://") + + @pytest.mark.asyncio + async def test_get_download_url_by_id( + self, file_attachments_api_async, uploaded_file_attachment + ): + """Test getting a download URL using file attachment ID.""" + url = await file_attachments_api_async.get_download_url( + file_attachment=uploaded_file_attachment.id_ + ) + + assert isinstance(url, str) + assert len(url) > 0 + + @pytest.mark.asyncio + async def test_download_file(self, file_attachments_api_async, uploaded_file_attachment): + """Test downloading a file attachment to a local path.""" + with tempfile.TemporaryDirectory() as tmpdir: + output_path = Path(tmpdir) / "downloaded_file.txt" + + # Download the file + await file_attachments_api_async.download( + file_attachment=uploaded_file_attachment, + output_path=output_path, + ) + + # Verify the file was downloaded + assert output_path.exists() + assert output_path.stat().st_size > 0 + + # Verify content + content = output_path.read_text() + assert "Test file content for integration tests" in content + + @pytest.mark.asyncio + async def test_download_file_by_id( + self, file_attachments_api_async, uploaded_file_attachment + ): + """Test downloading a file attachment using file attachment ID.""" + with tempfile.TemporaryDirectory() as tmpdir: + output_path = Path(tmpdir) / "downloaded_by_id.txt" + + # Download using ID + await file_attachments_api_async.download( + file_attachment=uploaded_file_attachment.id_, + output_path=str(output_path), # Test with string path + ) + + # Verify the file was downloaded + assert output_path.exists() + assert output_path.stat().st_size > 0 + + +class TestFileAttachmentsAPISync: + """Test suite for the synchronous File Attachments API functionality. + + Only includes a single test for basic sync generation. No specific sync behavior difference tests are needed. + """ + + class TestList: + """Tests for the sync list_ method.""" + + def test_basic_list(self, file_attachments_api_sync): + """Test basic synchronous file attachment listing functionality.""" + file_attachments = file_attachments_api_sync.list_(limit=5) + + # Verify we get a list + assert isinstance(file_attachments, list) + + # If we have file attachments, verify their structure + if file_attachments: + assert isinstance(file_attachments[0], FileAttachment) + + class TestUpload: + """Tests for the sync upload method.""" + + def test_upload_and_delete(self, file_attachments_api_sync, test_run): + """Test synchronous upload and cleanup.""" + # Create a temporary test file + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: + tmp.write("Sync test file\n") + tmp_path = tmp.name + + try: + # Upload using sync API + file_attachment = file_attachments_api_sync.upload( + path=tmp_path, + entity=test_run, + description="Sync upload test", + ) + + assert isinstance(file_attachment, FileAttachment) + assert file_attachment.id_ is not None + + # Cleanup + file_attachments_api_sync.delete(file_attachments=file_attachment) + finally: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 1094ddd0b..bda5c063d 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -7,6 +7,7 @@ from sift_client._internal.low_level_wrappers.upload import UploadLowLevelClient from sift_client.resources._base import ResourceBase from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate +from sift_client.util import cel_utils as cel if TYPE_CHECKING: from sift_client.client import SiftClient @@ -74,27 +75,35 @@ async def list_( Returns: A list of FileAttachments. """ + # Build filter parts + filter_parts = [] + if entity is not None: - entity_id = entity.id_ - entity_type = entity._get_entity_type_name() - kwargs = {} + filter_parts.append(cel.equals("entity_id", entity._id_or_error)) + filter_parts.append(cel.equals("entity_type", entity._get_entity_type_name())) + if remote_file_id: - kwargs["remote_file_id"] = remote_file_id + filter_parts.append(cel.equals("remote_file_id", remote_file_id)) + if file_name: - kwargs["file_name"] = file_name + filter_parts.append(cel.equals("file_name", file_name)) + if entity_id: - kwargs["entity_id"] = entity_id + filter_parts.append(cel.equals("entity_id", entity_id)) + if entity_type: - kwargs["entity_type"] = entity_type + filter_parts.append(cel.equals("entity_type", entity_type)) + + query_filter = cel.and_(*filter_parts) file_attachments = await self._low_level_client.list_all_remote_files( - kwargs=kwargs, + query_filter=query_filter or None, order_by=order_by, max_results=limit, page_size=page_size, sift_client=self.client, ) - return [self._apply_client_to_instance(fa) for fa in file_attachments] + return self._apply_client_to_instances(file_attachments) async def update( self, diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 8e7e97959..6f741e06c 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -12,7 +12,6 @@ if TYPE_CHECKING: import pandas as pd import pyarrow as pa - from sift_py.file_attachment.entity import Entity from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset, AssetUpdate @@ -611,7 +610,7 @@ class FileAttachmentsAPI: self, *, path: str | Path, - entity: Entity, + entity: Asset | Run | TestReport, metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, diff --git a/python/lib/sift_client/sift_types/file_attachment.py b/python/lib/sift_client/sift_types/file_attachment.py index 518e740ad..a9fd0e5a5 100644 --- a/python/lib/sift_client/sift_types/file_attachment.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -113,6 +113,10 @@ def entity(self) -> Run | Asset | TestReport: else: raise Exception(f"Unknown remote file entity type: {self.entity_type}") + def delete(self) -> None: + """Delete the file attachment.""" + self.client.file_attachments.delete(file_attachments=self) + def update(self, update: FileAttachmentUpdate | dict) -> FileAttachment: """Update the file attachment.""" if isinstance(update, dict): From 5033bf2b99b573d8a26046ba48e9722e9ad92b5d Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 15:37:50 -0800 Subject: [PATCH 082/127] lint fixes --- .../_tests/resources/test_file_attachments.py | 30 ++++++++++++++----- .../sift_client/resources/file_attachments.py | 17 +++++------ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index 97577fe08..93a3bb972 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -195,10 +195,14 @@ async def test_get_by_id(self, file_attachments_api_async, uploaded_file_attachm @pytest.mark.asyncio async def test_get_nonexistent_raises_error(self, file_attachments_api_async): """Test that getting a non-existent file attachment raises an error.""" - with pytest.raises(Exception): # Could be more specific based on actual error + # Should raise an error for non-existent file attachment + try: await file_attachments_api_async.get( file_attachment_id="nonexistent-file-id-12345" ) + pytest.fail("Expected an exception for non-existent file attachment") + except Exception: + pass # Expected - any exception is acceptable class TestList: """Tests for the async list_ method.""" @@ -343,9 +347,12 @@ async def test_delete_single_by_id(self, file_attachments_api_async, test_run): # Delete by ID string await file_attachments_api_async.delete(file_attachments=file_attachment.id_) - # Verify it's deleted - with pytest.raises(Exception): + # Verify it's deleted by attempting to get it (should raise error) + try: await file_attachments_api_async.get(file_attachment_id=file_attachment.id_) + pytest.fail("Expected file attachment to be deleted") + except Exception: + pass # Expected - file was deleted finally: if os.path.exists(tmp_path): os.unlink(tmp_path) @@ -368,9 +375,12 @@ async def test_delete_single_by_object(self, file_attachments_api_async, test_ru # Delete by FileAttachment object await file_attachments_api_async.delete(file_attachments=file_attachment) - # Verify it's deleted - with pytest.raises(Exception): + # Verify it's deleted by attempting to get it (should raise error) + try: await file_attachments_api_async.get(file_attachment_id=file_attachment.id_) + pytest.fail("Expected file attachment to be deleted") + except Exception: + pass # Expected - file was deleted finally: if os.path.exists(tmp_path): os.unlink(tmp_path) @@ -402,8 +412,11 @@ async def test_delete_multiple(self, file_attachments_api_async, test_run): # Verify they're all deleted for fa in file_attachments: - with pytest.raises(Exception): + try: await file_attachments_api_async.get(file_attachment_id=fa.id_) + pytest.fail(f"Expected file attachment {fa.id_} to be deleted") + except Exception: # noqa: PERF203 + pass # Expected - file was deleted finally: for tmp_path in tmp_paths: if os.path.exists(tmp_path): @@ -437,8 +450,11 @@ async def test_delete_list_of_ids(self, file_attachments_api_async, test_run): # Verify they're all deleted for fa_id in ids: - with pytest.raises(Exception): + try: await file_attachments_api_async.get(file_attachment_id=fa_id) + pytest.fail(f"Expected file attachment {fa_id} to be deleted") + except Exception: # noqa: PERF203 + pass # Expected - file was deleted finally: for tmp_path in tmp_paths: if os.path.exists(tmp_path): diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index bda5c063d..03c6fa228 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -77,23 +77,20 @@ async def list_( """ # Build filter parts filter_parts = [] - + if entity is not None: filter_parts.append(cel.equals("entity_id", entity._id_or_error)) filter_parts.append(cel.equals("entity_type", entity._get_entity_type_name())) - + else: + if entity_id: + filter_parts.append(cel.equals("entity_id", entity_id)) + if entity_type: + filter_parts.append(cel.equals("entity_type", entity_type)) if remote_file_id: filter_parts.append(cel.equals("remote_file_id", remote_file_id)) - if file_name: filter_parts.append(cel.equals("file_name", file_name)) - - if entity_id: - filter_parts.append(cel.equals("entity_id", entity_id)) - - if entity_type: - filter_parts.append(cel.equals("entity_type", entity_type)) - + query_filter = cel.and_(*filter_parts) file_attachments = await self._low_level_client.list_all_remote_files( From e2aeac895e593e1c423b0b1db0583fc4b5a6ee60 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 15:39:39 -0800 Subject: [PATCH 083/127] format --- .../_tests/resources/test_file_attachments.py | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index 93a3bb972..c1c35c533 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -197,9 +197,7 @@ async def test_get_nonexistent_raises_error(self, file_attachments_api_async): """Test that getting a non-existent file attachment raises an error.""" # Should raise an error for non-existent file attachment try: - await file_attachments_api_async.get( - file_attachment_id="nonexistent-file-id-12345" - ) + await file_attachments_api_async.get(file_attachment_id="nonexistent-file-id-12345") pytest.fail("Expected an exception for non-existent file attachment") except Exception: pass # Expected - any exception is acceptable @@ -310,9 +308,7 @@ async def test_update_description( assert updated.description == new_description @pytest.mark.asyncio - async def test_update_with_dict( - self, file_attachments_api_async, uploaded_file_attachment - ): + async def test_update_with_dict(self, file_attachments_api_async, uploaded_file_attachment): """Test updating a file attachment using a dict.""" new_description = "Updated via dict" @@ -394,9 +390,7 @@ async def test_delete_multiple(self, file_attachments_api_async, test_run): try: for i in range(3): - with tempfile.NamedTemporaryFile( - mode="w", suffix=".txt", delete=False - ) as tmp: + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: tmp.write(f"File {i} to delete\n") tmp_paths.append(tmp.name) @@ -431,9 +425,7 @@ async def test_delete_list_of_ids(self, file_attachments_api_async, test_run): try: for i in range(2): - with tempfile.NamedTemporaryFile( - mode="w", suffix=".txt", delete=False - ) as tmp: + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: tmp.write(f"File {i} to delete\n") tmp_paths.append(tmp.name) @@ -464,9 +456,7 @@ class TestDownload: """Tests for the async download methods.""" @pytest.mark.asyncio - async def test_get_download_url( - self, file_attachments_api_async, uploaded_file_attachment - ): + async def test_get_download_url(self, file_attachments_api_async, uploaded_file_attachment): """Test getting a download URL for a file attachment.""" url = await file_attachments_api_async.get_download_url( file_attachment=uploaded_file_attachment @@ -574,4 +564,3 @@ def test_upload_and_delete(self, file_attachments_api_sync, test_run): finally: if os.path.exists(tmp_path): os.unlink(tmp_path) - From 009bb9ac094b09903801c779a248cc0e9157234b Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 15:44:54 -0800 Subject: [PATCH 084/127] type checking --- .../lib/sift_client/resources/sync_stubs/__init__.pyi | 4 ++-- .../sift_types/_mixins/file_attachments.py | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 6f741e06c..a10e61b0e 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -569,7 +569,7 @@ class FileAttachmentsAPI: def list_( self, *, - entity: Run | Asset | TestReport | None = None, + entity: _SupportsAttachments | Run | Asset | TestReport | None = None, remote_file_id: str | None = None, file_name: str | None = None, entity_type: str | None = None, @@ -610,7 +610,7 @@ class FileAttachmentsAPI: self, *, path: str | Path, - entity: Asset | Run | TestReport, + entity: _SupportsAttachments | Asset | Run | TestReport, metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 41d242f92..9679cd8c7 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -8,7 +8,10 @@ from pathlib import Path from sift_client.client import SiftClient + from sift_client.sift_types.asset import Asset from sift_client.sift_types.file_attachment import FileAttachment + from sift_client.sift_types.run import Run + from sift_client.sift_types.test_report import TestReport class _SupportsFileAttachments(Protocol): @@ -104,13 +107,11 @@ def upload_attachment( """ if not self.id_: raise ValueError("Entity ID is not set") - entity = Entity( - entity_id=self.id_, - entity_type=self._get_entity_type_name(), # type: ignore[attr-defined] - ) + if not isinstance(self, (Asset, Run, TestReport)): + raise ValueError("Entity is not a valid entity type") return self.client.file_attachments.upload( path=path, - entity=entity, + entity=self, metadata=metadata, description=description, organization_id=organization_id, From 65dcad7c6b0a704f900c6c09db5b8072b069069c Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 15:55:31 -0800 Subject: [PATCH 085/127] small fix --- .../lib/sift_client/resources/sync_stubs/__init__.pyi | 5 +++-- .../sift_client/sift_types/_mixins/file_attachments.py | 10 ++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index a10e61b0e..a58fba6be 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -14,6 +14,7 @@ if TYPE_CHECKING: import pyarrow as pa from sift_client.client import SiftClient + from sift_client.sift_types._mixins.file_attachments import _SupportsFileAttachments from sift_client.sift_types.asset import Asset, AssetUpdate from sift_client.sift_types.calculated_channel import ( CalculatedChannel, @@ -569,7 +570,7 @@ class FileAttachmentsAPI: def list_( self, *, - entity: _SupportsAttachments | Run | Asset | TestReport | None = None, + entity: _SupportsFileAttachments, remote_file_id: str | None = None, file_name: str | None = None, entity_type: str | None = None, @@ -610,7 +611,7 @@ class FileAttachmentsAPI: self, *, path: str | Path, - entity: _SupportsAttachments | Asset | Run | TestReport, + entity: _SupportsFileAttachments, metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 9679cd8c7..72dc4f410 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -2,16 +2,15 @@ from typing import TYPE_CHECKING, Any, ClassVar, Protocol -from sift_py.file_attachment.entity import Entity +from sift_client.sift_types.asset import Asset +from sift_client.sift_types.run import Run +from sift_client.sift_types.test_report import TestReport if TYPE_CHECKING: from pathlib import Path from sift_client.client import SiftClient - from sift_client.sift_types.asset import Asset from sift_client.sift_types.file_attachment import FileAttachment - from sift_client.sift_types.run import Run - from sift_client.sift_types.test_report import TestReport class _SupportsFileAttachments(Protocol): @@ -72,8 +71,7 @@ def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]: A list of FileAttachments associated with this entity. """ return self.client.file_attachments.list_( - entity_type=self._get_entity_type_name(), # type: ignore[attr-defined] - entity_id=self.id_, + entity=self, ) def delete_attachment( From 8580896d3d52c1ca5997324e67eaf6893bdf1ee6 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 15:58:46 -0800 Subject: [PATCH 086/127] small fix --- .../lib/sift_client/_tests/resources/test_file_attachments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index c1c35c533..be1bab210 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -15,7 +15,7 @@ from sift_client import SiftClient from sift_client.resources import FileAttachmentsAPI, FileAttachmentsAPIAsync -from sift_client.sift_types import FileAttachment, FileAttachmentUpdate +from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate pytestmark = pytest.mark.integration From 45e4376b544f70e2e1871f96a8f5e6a44b8db961 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 16:13:02 -0800 Subject: [PATCH 087/127] small fix --- .../_tests/resources/test_file_attachments.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index be1bab210..12e826761 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -297,10 +297,8 @@ async def test_update_description( """Test updating a file attachment's description.""" new_description = "Updated description for integration test" - update = FileAttachmentUpdate( - id_=uploaded_file_attachment.id_, - description=new_description, - ) + update = FileAttachmentUpdate(description=new_description) + update.resource_id = uploaded_file_attachment.id_ updated = await file_attachments_api_async.update(file_attachment=update) @@ -312,12 +310,12 @@ async def test_update_with_dict(self, file_attachments_api_async, uploaded_file_ """Test updating a file attachment using a dict.""" new_description = "Updated via dict" - updated = await file_attachments_api_async.update( - file_attachment={ - "id_": uploaded_file_attachment.id_, - "description": new_description, - } - ) + # When using dict, the ID must be set via resource_id after creating the update object + update_dict = {"description": new_description} + update = FileAttachmentUpdate.model_validate(update_dict) + update.resource_id = uploaded_file_attachment.id_ + + updated = await file_attachments_api_async.update(file_attachment=update) assert updated.id_ == uploaded_file_attachment.id_ assert updated.description == new_description From 17514ad087783cb523cffa8c3e5da8111b0c3da6 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 16:15:27 -0800 Subject: [PATCH 088/127] white space --- .../lib/sift_client/_tests/resources/test_file_attachments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index 12e826761..339d2658a 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -314,7 +314,7 @@ async def test_update_with_dict(self, file_attachments_api_async, uploaded_file_ update_dict = {"description": new_description} update = FileAttachmentUpdate.model_validate(update_dict) update.resource_id = uploaded_file_attachment.id_ - + updated = await file_attachments_api_async.update(file_attachment=update) assert updated.id_ == uploaded_file_attachment.id_ From e860741fc5b9cd3b3aa6716d0ff2c3592328c241 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 16:23:27 -0800 Subject: [PATCH 089/127] circular import --- python/lib/sift_client/resources/file_attachments.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 03c6fa228..860f77640 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -6,12 +6,12 @@ from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient from sift_client._internal.low_level_wrappers.upload import UploadLowLevelClient from sift_client.resources._base import ResourceBase -from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate from sift_client.util import cel_utils as cel if TYPE_CHECKING: from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset + from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate from sift_client.sift_types.run import Run from sift_client.sift_types.test_report import TestReport @@ -42,6 +42,8 @@ async def get(self, *, file_attachment_id: str) -> FileAttachment: Returns: The FileAttachment. """ + from sift_client.sift_types.file_attachment import FileAttachment + file_attachment = await self._low_level_client.get_remote_file( remote_file_id=file_attachment_id, sift_client=self.client, @@ -115,6 +117,8 @@ async def update( Returns: The updated FileAttachment. """ + from sift_client.sift_types.file_attachment import FileAttachmentUpdate + if isinstance(file_attachment, dict): file_attachment = FileAttachmentUpdate.model_validate(file_attachment) @@ -132,6 +136,8 @@ async def delete( Args: file_attachments: List of FileAttachments or the IDs of the file attachments to delete (up to 1000). """ + from sift_client.sift_types.file_attachment import FileAttachment + file_attachment_ids: list[str] = [] if isinstance(file_attachments, FileAttachment): file_attachment_ids.append(file_attachments._id_or_error) @@ -162,6 +168,8 @@ async def get_download_url(self, *, file_attachment: FileAttachment | str) -> st Returns: The download URL for the file attachment. """ + from sift_client.sift_types.file_attachment import FileAttachment + attachment_id = ( file_attachment._id_or_error if isinstance(file_attachment, FileAttachment) From 3deb30a6737dcdb4b4fefb993b73622c7e0d76ca Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 16:25:19 -0800 Subject: [PATCH 090/127] small fixes --- python/lib/sift_client/resources/file_attachments.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 860f77640..efa81841e 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -42,8 +42,6 @@ async def get(self, *, file_attachment_id: str) -> FileAttachment: Returns: The FileAttachment. """ - from sift_client.sift_types.file_attachment import FileAttachment - file_attachment = await self._low_level_client.get_remote_file( remote_file_id=file_attachment_id, sift_client=self.client, @@ -118,7 +116,7 @@ async def update( The updated FileAttachment. """ from sift_client.sift_types.file_attachment import FileAttachmentUpdate - + if isinstance(file_attachment, dict): file_attachment = FileAttachmentUpdate.model_validate(file_attachment) @@ -137,7 +135,7 @@ async def delete( file_attachments: List of FileAttachments or the IDs of the file attachments to delete (up to 1000). """ from sift_client.sift_types.file_attachment import FileAttachment - + file_attachment_ids: list[str] = [] if isinstance(file_attachments, FileAttachment): file_attachment_ids.append(file_attachments._id_or_error) @@ -169,7 +167,7 @@ async def get_download_url(self, *, file_attachment: FileAttachment | str) -> st The download URL for the file attachment. """ from sift_client.sift_types.file_attachment import FileAttachment - + attachment_id = ( file_attachment._id_or_error if isinstance(file_attachment, FileAttachment) From 38865501d3622202e1ed76d7e1bda8fe5fd94ffd Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 16:34:06 -0800 Subject: [PATCH 091/127] test fix --- .../_internal/low_level_wrappers/remote_files.py | 4 +++- .../sift_types/_mixins/file_attachments.py | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 3412213f6..9b97829da 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -18,11 +18,11 @@ from sift_client._internal.low_level_wrappers.base import ( LowLevelClientBase, ) -from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate from sift_client.transport import GrpcClient, WithGrpcClient if TYPE_CHECKING: from sift_client.client import SiftClient + from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate class RemoteFilesLowLevelClient(LowLevelClientBase, WithGrpcClient): @@ -137,6 +137,8 @@ async def update_remote_file( Returns: The updated RemoteFile. """ + from sift_client.sift_types.file_attachment import FileAttachment + grpc_remote_file, update_mask = update.to_proto_with_mask() request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask) response = await self._grpc_client.get_stub(RemoteFileServiceStub).UpdateRemoteFile(request) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 72dc4f410..5380b1455 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -2,15 +2,14 @@ from typing import TYPE_CHECKING, Any, ClassVar, Protocol -from sift_client.sift_types.asset import Asset -from sift_client.sift_types.run import Run -from sift_client.sift_types.test_report import TestReport - if TYPE_CHECKING: from pathlib import Path from sift_client.client import SiftClient + from sift_client.sift_types.asset import Asset from sift_client.sift_types.file_attachment import FileAttachment + from sift_client.sift_types.run import Run + from sift_client.sift_types.test_report import TestReport class _SupportsFileAttachments(Protocol): @@ -105,8 +104,9 @@ def upload_attachment( """ if not self.id_: raise ValueError("Entity ID is not set") - if not isinstance(self, (Asset, Run, TestReport)): - raise ValueError("Entity is not a valid entity type") + # Check if the entity type is supported by checking the class name + if self.__class__.__name__ not in ("Asset", "Run", "TestReport"): + raise ValueError(f"Entity type {self.__class__.__name__} is not supported for file attachments") return self.client.file_attachments.upload( path=path, entity=self, From 5a5129b49cf2a52dd5224d2fa69685eb972e8c3c Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 16:37:20 -0800 Subject: [PATCH 092/127] small fixes --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 2 +- python/lib/sift_client/sift_types/_mixins/file_attachments.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 9b97829da..6db91ab8d 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -138,7 +138,7 @@ async def update_remote_file( The updated RemoteFile. """ from sift_client.sift_types.file_attachment import FileAttachment - + grpc_remote_file, update_mask = update.to_proto_with_mask() request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask) response = await self._grpc_client.get_stub(RemoteFileServiceStub).UpdateRemoteFile(request) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 5380b1455..98ed6db3e 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -6,10 +6,7 @@ from pathlib import Path from sift_client.client import SiftClient - from sift_client.sift_types.asset import Asset from sift_client.sift_types.file_attachment import FileAttachment - from sift_client.sift_types.run import Run - from sift_client.sift_types.test_report import TestReport class _SupportsFileAttachments(Protocol): From 599d14bfe0f13cd5f944d3ccf22022739c1d534d Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 16:40:47 -0800 Subject: [PATCH 093/127] format --- python/lib/sift_client/sift_types/_mixins/file_attachments.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 98ed6db3e..5c6bd5424 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -103,7 +103,9 @@ def upload_attachment( raise ValueError("Entity ID is not set") # Check if the entity type is supported by checking the class name if self.__class__.__name__ not in ("Asset", "Run", "TestReport"): - raise ValueError(f"Entity type {self.__class__.__name__} is not supported for file attachments") + raise ValueError( + f"Entity type {self.__class__.__name__} is not supported for file attachments" + ) return self.client.file_attachments.upload( path=path, entity=self, From 44124f1f74ca6ce7f8eb59ecd6ff27b95f2949e0 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 16:45:31 -0800 Subject: [PATCH 094/127] update --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index a58fba6be..6f741e06c 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -14,7 +14,6 @@ if TYPE_CHECKING: import pyarrow as pa from sift_client.client import SiftClient - from sift_client.sift_types._mixins.file_attachments import _SupportsFileAttachments from sift_client.sift_types.asset import Asset, AssetUpdate from sift_client.sift_types.calculated_channel import ( CalculatedChannel, @@ -570,7 +569,7 @@ class FileAttachmentsAPI: def list_( self, *, - entity: _SupportsFileAttachments, + entity: Run | Asset | TestReport | None = None, remote_file_id: str | None = None, file_name: str | None = None, entity_type: str | None = None, @@ -611,7 +610,7 @@ class FileAttachmentsAPI: self, *, path: str | Path, - entity: _SupportsFileAttachments, + entity: Asset | Run | TestReport, metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, From 9135862e606868a25b687d050bf1c0104d9bb4f9 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 16:51:39 -0800 Subject: [PATCH 095/127] fix --- .../lib/sift_client/resources/file_attachments.py | 1 + .../sift_client/resources/sync_stubs/__init__.pyi | 3 ++- .../sift_types/_mixins/file_attachments.py | 15 ++++++++------- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index efa81841e..dee6ed337 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from sift_client.client import SiftClient + from sift_client.sift_types._mixins.file_attachments import _SupportsFileAttachments from sift_client.sift_types.asset import Asset from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate from sift_client.sift_types.run import Run diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 6f741e06c..c9210c2ac 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -14,6 +14,7 @@ if TYPE_CHECKING: import pyarrow as pa from sift_client.client import SiftClient + from sift_client.sift_types._mixins.file_attachments import _SupportsFileAttachments from sift_client.sift_types.asset import Asset, AssetUpdate from sift_client.sift_types.calculated_channel import ( CalculatedChannel, @@ -610,7 +611,7 @@ class FileAttachmentsAPI: self, *, path: str | Path, - entity: Asset | Run | TestReport, + entity: _SupportsFileAttachments | Asset | Run | TestReport, metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 5c6bd5424..e1f95fa57 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -2,6 +2,10 @@ from typing import TYPE_CHECKING, Any, ClassVar, Protocol +from sift_client.sift_types.asset import Asset +from sift_client.sift_types.run import Run +from sift_client.sift_types.test_report import TestReport + if TYPE_CHECKING: from pathlib import Path @@ -66,6 +70,8 @@ def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]: Returns: A list of FileAttachments associated with this entity. """ + if not isinstance(self, (Asset, Run, TestReport)): + raise ValueError("Entity is not a valid entity type") return self.client.file_attachments.list_( entity=self, ) @@ -99,13 +105,8 @@ def upload_attachment( Returns: The uploaded FileAttachment. """ - if not self.id_: - raise ValueError("Entity ID is not set") - # Check if the entity type is supported by checking the class name - if self.__class__.__name__ not in ("Asset", "Run", "TestReport"): - raise ValueError( - f"Entity type {self.__class__.__name__} is not supported for file attachments" - ) + if not isinstance(self, (Asset, Run, TestReport)): + raise ValueError("Entity is not a valid entity type") return self.client.file_attachments.upload( path=path, entity=self, From cdcad4696fb903fe7055f0c1d856e05f8c0af928 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 16:52:13 -0800 Subject: [PATCH 096/127] remove import --- python/lib/sift_client/resources/file_attachments.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index dee6ed337..efa81841e 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -10,7 +10,6 @@ if TYPE_CHECKING: from sift_client.client import SiftClient - from sift_client.sift_types._mixins.file_attachments import _SupportsFileAttachments from sift_client.sift_types.asset import Asset from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate from sift_client.sift_types.run import Run From 59b1bfe2a38cc71735a15611f04a02b8bc51883c Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 17:06:35 -0800 Subject: [PATCH 097/127] small fix --- .../sift_types/_mixins/file_attachments.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index e1f95fa57..bf49fea9f 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -2,15 +2,14 @@ from typing import TYPE_CHECKING, Any, ClassVar, Protocol -from sift_client.sift_types.asset import Asset -from sift_client.sift_types.run import Run -from sift_client.sift_types.test_report import TestReport - if TYPE_CHECKING: from pathlib import Path from sift_client.client import SiftClient + from sift_client.sift_types.asset import Asset from sift_client.sift_types.file_attachment import FileAttachment + from sift_client.sift_types.run import Run + from sift_client.sift_types.test_report import TestReport class _SupportsFileAttachments(Protocol): @@ -70,7 +69,7 @@ def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]: Returns: A list of FileAttachments associated with this entity. """ - if not isinstance(self, (Asset, Run, TestReport)): + if self.__class__.__name__ not in self._ENTITY_TYPE_MAP: raise ValueError("Entity is not a valid entity type") return self.client.file_attachments.list_( entity=self, @@ -105,7 +104,7 @@ def upload_attachment( Returns: The uploaded FileAttachment. """ - if not isinstance(self, (Asset, Run, TestReport)): + if self.__class__.__name__ not in self._ENTITY_TYPE_MAP: raise ValueError("Entity is not a valid entity type") return self.client.file_attachments.upload( path=path, From f77302e0c5d11c7f7156f41f47c97531b5073685 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 17:32:00 -0800 Subject: [PATCH 098/127] format --- python/lib/sift_client/sift_types/_mixins/file_attachments.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index bf49fea9f..41541e715 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -6,10 +6,7 @@ from pathlib import Path from sift_client.client import SiftClient - from sift_client.sift_types.asset import Asset from sift_client.sift_types.file_attachment import FileAttachment - from sift_client.sift_types.run import Run - from sift_client.sift_types.test_report import TestReport class _SupportsFileAttachments(Protocol): From 53f9be76c7732c3e60076804f7bfaf46850b5d26 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 17:38:08 -0800 Subject: [PATCH 099/127] small fixes --- .../lib/sift_client/sift_types/_mixins/file_attachments.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 41541e715..0190e9dc0 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -6,7 +6,10 @@ from pathlib import Path from sift_client.client import SiftClient + from sift_client.sift_types.asset import Asset from sift_client.sift_types.file_attachment import FileAttachment + from sift_client.sift_types.run import Run + from sift_client.sift_types.test_report import TestReport class _SupportsFileAttachments(Protocol): @@ -66,7 +69,7 @@ def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]: Returns: A list of FileAttachments associated with this entity. """ - if self.__class__.__name__ not in self._ENTITY_TYPE_MAP: + if not isinstance(self, (Asset, Run, TestReport)): raise ValueError("Entity is not a valid entity type") return self.client.file_attachments.list_( entity=self, @@ -101,7 +104,7 @@ def upload_attachment( Returns: The uploaded FileAttachment. """ - if self.__class__.__name__ not in self._ENTITY_TYPE_MAP: + if not isinstance(self, (Asset, Run, TestReport)): raise ValueError("Entity is not a valid entity type") return self.client.file_attachments.upload( path=path, From 0ad15dee772a8b457d76bc853815a1e4d8ccd0c7 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 17:40:58 -0800 Subject: [PATCH 100/127] fix` --- .../lib/sift_client/sift_types/_mixins/file_attachments.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 0190e9dc0..e1f95fa57 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -2,14 +2,15 @@ from typing import TYPE_CHECKING, Any, ClassVar, Protocol +from sift_client.sift_types.asset import Asset +from sift_client.sift_types.run import Run +from sift_client.sift_types.test_report import TestReport + if TYPE_CHECKING: from pathlib import Path from sift_client.client import SiftClient - from sift_client.sift_types.asset import Asset from sift_client.sift_types.file_attachment import FileAttachment - from sift_client.sift_types.run import Run - from sift_client.sift_types.test_report import TestReport class _SupportsFileAttachments(Protocol): From 05d5f7b0962e77f141a8190ce21e8ef2b99ca391 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Fri, 14 Nov 2025 17:52:38 -0800 Subject: [PATCH 101/127] small fix --- .../sift_types/_mixins/file_attachments.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index e1f95fa57..28278016a 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -2,10 +2,6 @@ from typing import TYPE_CHECKING, Any, ClassVar, Protocol -from sift_client.sift_types.asset import Asset -from sift_client.sift_types.run import Run -from sift_client.sift_types.test_report import TestReport - if TYPE_CHECKING: from pathlib import Path @@ -70,6 +66,10 @@ def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]: Returns: A list of FileAttachments associated with this entity. """ + from sift_client.sift_types.asset import Asset + from sift_client.sift_types.run import Run + from sift_client.sift_types.test_report import TestReport + if not isinstance(self, (Asset, Run, TestReport)): raise ValueError("Entity is not a valid entity type") return self.client.file_attachments.list_( @@ -105,6 +105,10 @@ def upload_attachment( Returns: The uploaded FileAttachment. """ + from sift_client.sift_types.asset import Asset + from sift_client.sift_types.run import Run + from sift_client.sift_types.test_report import TestReport + if not isinstance(self, (Asset, Run, TestReport)): raise ValueError("Entity is not a valid entity type") return self.client.file_attachments.upload( From b0291906d58997091942a05f87aba86150279bc4 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 17 Nov 2025 09:14:40 -0800 Subject: [PATCH 102/127] update stubs --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index c9210c2ac..6f741e06c 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -14,7 +14,6 @@ if TYPE_CHECKING: import pyarrow as pa from sift_client.client import SiftClient - from sift_client.sift_types._mixins.file_attachments import _SupportsFileAttachments from sift_client.sift_types.asset import Asset, AssetUpdate from sift_client.sift_types.calculated_channel import ( CalculatedChannel, @@ -611,7 +610,7 @@ class FileAttachmentsAPI: self, *, path: str | Path, - entity: _SupportsFileAttachments | Asset | Run | TestReport, + entity: Asset | Run | TestReport, metadata: dict[str, Any] | None = None, description: str | None = None, organization_id: str | None = None, From 1d9380f46c1bc0b1eaa5a7ecf5310fe973037150 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 17 Nov 2025 09:28:49 -0800 Subject: [PATCH 103/127] test fixes --- python/lib/sift_client/_tests/sift_types/test_asset.py | 3 +-- python/lib/sift_client/_tests/sift_types/test_results.py | 3 +-- python/lib/sift_client/_tests/sift_types/test_run.py | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/python/lib/sift_client/_tests/sift_types/test_asset.py b/python/lib/sift_client/_tests/sift_types/test_asset.py index 667760410..d95fda260 100644 --- a/python/lib/sift_client/_tests/sift_types/test_asset.py +++ b/python/lib/sift_client/_tests/sift_types/test_asset.py @@ -179,8 +179,7 @@ def test_attachments_property_fetches_files(self, mock_asset, mock_client): # Verify file_attachments.list_ was called with correct parameters mock_client.file_attachments.list_.assert_called_once_with( - entity_type="ENTITY_TYPE_ASSET", - entity_id=mock_asset.id_, + entity=mock_asset, ) # Verify result diff --git a/python/lib/sift_client/_tests/sift_types/test_results.py b/python/lib/sift_client/_tests/sift_types/test_results.py index 1bee37df5..ac9d8309c 100644 --- a/python/lib/sift_client/_tests/sift_types/test_results.py +++ b/python/lib/sift_client/_tests/sift_types/test_results.py @@ -225,8 +225,7 @@ def test_attachments_property_fetches_files(self, mock_test_report, mock_client) # Verify file_attachments.list_ was called with correct parameters mock_client.file_attachments.list_.assert_called_once_with( - entity_type="ENTITY_TYPE_TEST_REPORT", - entity_id=mock_test_report.id_, + entity=mock_test_report, ) # Verify result diff --git a/python/lib/sift_client/_tests/sift_types/test_run.py b/python/lib/sift_client/_tests/sift_types/test_run.py index 34dd77b30..fa2ea3207 100644 --- a/python/lib/sift_client/_tests/sift_types/test_run.py +++ b/python/lib/sift_client/_tests/sift_types/test_run.py @@ -221,8 +221,7 @@ def test_remote_files_property_fetches_files(self, mock_run, mock_client): # Verify file_attachments.list_ was called with correct parameters mock_client.file_attachments.list_.assert_called_once_with( - entity_type="ENTITY_TYPE_RUN", - entity_id=mock_run.id_, + entity=mock_run, ) # Verify result @@ -243,8 +242,7 @@ def test_attachments_property_fetches_files(self, mock_run, mock_client): # Verify file_attachments.list_ was called with correct parameters mock_client.file_attachments.list_.assert_called_once_with( - entity_type="ENTITY_TYPE_RUN", - entity_id=mock_run.id_, + entity=mock_run, ) # Verify result From 1f228185dd87bee8aaee965b15f73104b7a92910 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 17 Nov 2025 13:08:42 -0800 Subject: [PATCH 104/127] Update tests --- .../_tests/resources/test_file_attachments.py | 41 +------------------ .../sift_client/resources/file_attachments.py | 2 +- .../resources/sync_stubs/__init__.pyi | 2 +- .../sift_types/_mixins/file_attachments.py | 19 ++++----- 4 files changed, 13 insertions(+), 51 deletions(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index 339d2658a..dd1d742b6 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -12,6 +12,7 @@ from pathlib import Path import pytest +import pytest_asyncio from sift_client import SiftClient from sift_client.resources import FileAttachmentsAPI, FileAttachmentsAPIAsync @@ -58,7 +59,7 @@ def test_asset(sift_client: SiftClient): pytest.skip("No assets available for testing") -@pytest.fixture +@pytest_asyncio.fixture async def uploaded_file_attachment(file_attachments_api_async, test_run): """Upload a test file and return the file attachment, cleaning up after test.""" # Create a temporary test file @@ -477,44 +478,6 @@ async def test_get_download_url_by_id( assert isinstance(url, str) assert len(url) > 0 - @pytest.mark.asyncio - async def test_download_file(self, file_attachments_api_async, uploaded_file_attachment): - """Test downloading a file attachment to a local path.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "downloaded_file.txt" - - # Download the file - await file_attachments_api_async.download( - file_attachment=uploaded_file_attachment, - output_path=output_path, - ) - - # Verify the file was downloaded - assert output_path.exists() - assert output_path.stat().st_size > 0 - - # Verify content - content = output_path.read_text() - assert "Test file content for integration tests" in content - - @pytest.mark.asyncio - async def test_download_file_by_id( - self, file_attachments_api_async, uploaded_file_attachment - ): - """Test downloading a file attachment using file attachment ID.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "downloaded_by_id.txt" - - # Download using ID - await file_attachments_api_async.download( - file_attachment=uploaded_file_attachment.id_, - output_path=str(output_path), # Test with string path - ) - - # Verify the file was downloaded - assert output_path.exists() - assert output_path.stat().st_size > 0 - class TestFileAttachmentsAPISync: """Test suite for the synchronous File Attachments API functionality. diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index efa81841e..d7210ac12 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -66,7 +66,7 @@ async def list_( entity: Filter by entity (Run, Asset, or TestReport). remote_file_id: Filter by remote file ID. file_name: Filter by file name. - entity_type: Filter by entity type (e.g., 'ENTITY_TYPE_ASSET', 'ENTITY_TYPE_RUN'). + entity_type: Filter by entity type enum value (e.g., 1 for Run, 3 for Asset, 5 for TestReport). entity_id: Filter by entity ID. order_by: The field to order by. limit: Maximum number of results to return. diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 6f741e06c..48aaae179 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -584,7 +584,7 @@ class FileAttachmentsAPI: entity: Filter by entity (Run, Asset, or TestReport). remote_file_id: Filter by remote file ID. file_name: Filter by file name. - entity_type: Filter by entity type (e.g., 'ENTITY_TYPE_ASSET', 'ENTITY_TYPE_RUN'). + entity_type: Filter by entity type enum value (e.g., 1 for Run, 3 for Asset, 5 for TestReport). entity_id: Filter by entity ID. order_by: The field to order by. limit: Maximum number of results to return. diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 28278016a..56d9538a2 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -27,23 +27,22 @@ class FileAttachmentsMixin: - client: SiftClient property The entity type is automatically determined from the class name: - - Asset -> ENTITY_TYPE_ASSET - - Run -> ENTITY_TYPE_RUN - - TestReport -> ENTITY_TYPE_TEST_REPORT + - Asset -> assets + - Run -> runs + - TestReport -> test_reports """ - # Mapping of class names to entity types + # Mapping of class names to entity types (REST API format) _ENTITY_TYPE_MAP: ClassVar[dict[str, str]] = { - "Asset": "ENTITY_TYPE_ASSET", - "Run": "ENTITY_TYPE_RUN", - "TestReport": "ENTITY_TYPE_TEST_REPORT", + "Asset": "assets", + "Run": "runs", + "TestReport": "test_reports", } - def _get_entity_type_name(self) -> str: - """Get the entity type for filtering based on the class name. + """Get the entity type string. Returns: - The entity type string (e.g., 'ENTITY_TYPE_ASSET', 'ENTITY_TYPE_RUN') + The entity type string (e.g., 'assets', 'runs', 'test_reports') Raises: ValueError: If the class name is not in the entity type mapping. From fd70963dd0e30bf32f64a711099bbc823210960a Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 17 Nov 2025 13:34:30 -0800 Subject: [PATCH 105/127] Update integration tests --- python/lib/sift_client/_tests/README.md | 13 +++++++++++++ python/lib/sift_client/_tests/conftest.py | 4 +++- .../sift_types/_mixins/file_attachments.py | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 python/lib/sift_client/_tests/README.md diff --git a/python/lib/sift_client/_tests/README.md b/python/lib/sift_client/_tests/README.md new file mode 100644 index 000000000..f5f6360f3 --- /dev/null +++ b/python/lib/sift_client/_tests/README.md @@ -0,0 +1,13 @@ +# Sift Python + +## Running Integration Tests Locally + +1. Create Environmental Variables + a. Create or open a .env file in /python + b. Add an API key for SIFT_API_KEY +2. Start Local Sift +3. Asset Data: NostromoLV426 + a. Ensure that your local Sift instance contains data for the asset NostromoLV426 + b. If it doesn't them export data for NostromoLV426 from development +4. Run tests + a. Run tests using /python/scripts/dev {test, test-integration, test-all} diff --git a/python/lib/sift_client/_tests/conftest.py b/python/lib/sift_client/_tests/conftest.py index ee2ebc4cb..3efaf4d93 100644 --- a/python/lib/sift_client/_tests/conftest.py +++ b/python/lib/sift_client/_tests/conftest.py @@ -19,13 +19,15 @@ def sift_client() -> SiftClient: grpc_url = os.getenv("SIFT_GRPC_URI", "localhost:50051") rest_url = os.getenv("SIFT_REST_URI", "localhost:8080") api_key = os.getenv("SIFT_API_KEY", "") + # If the URL contains localhost, don't use SSL. Most likely running tests or local development. + use_ssl = not ("localhost" in grpc_url or "localhost" in rest_url) client = SiftClient( connection_config=SiftConnectionConfig( api_key=api_key, grpc_url=grpc_url, rest_url=rest_url, - use_ssl=True, + use_ssl=use_ssl, ) ) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 56d9538a2..39b0d5166 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -38,6 +38,7 @@ class FileAttachmentsMixin: "Run": "runs", "TestReport": "test_reports", } + def _get_entity_type_name(self) -> str: """Get the entity type string. From 792a624182c8262afd7fce1d3fb0e9a7385e2e04 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 18 Nov 2025 15:34:32 -0800 Subject: [PATCH 106/127] small fixes --- .../_tests/resources/test_file_attachments.py | 42 +++++++++---------- .../sift_client/resources/file_attachments.py | 6 +-- .../sift_client/sift_types/file_attachment.py | 3 +- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index dd1d742b6..1fffc4271 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -47,7 +47,7 @@ def test_run(sift_client: SiftClient): runs = sift_client.runs.list_(limit=1) if runs: return runs[0] - pytest.skip("No runs available for testing") + pytest.fail("No runs available for testing, please add test test runs") @pytest.fixture @@ -56,7 +56,7 @@ def test_asset(sift_client: SiftClient): assets = sift_client.assets.list_(limit=1) if assets: return assets[0] - pytest.skip("No assets available for testing") + pytest.fail("No assets available for testing, please add test test assets") @pytest_asyncio.fixture @@ -197,11 +197,8 @@ async def test_get_by_id(self, file_attachments_api_async, uploaded_file_attachm async def test_get_nonexistent_raises_error(self, file_attachments_api_async): """Test that getting a non-existent file attachment raises an error.""" # Should raise an error for non-existent file attachment - try: + with pytest.raises(Exception): await file_attachments_api_async.get(file_attachment_id="nonexistent-file-id-12345") - pytest.fail("Expected an exception for non-existent file attachment") - except Exception: - pass # Expected - any exception is acceptable class TestList: """Tests for the async list_ method.""" @@ -343,11 +340,8 @@ async def test_delete_single_by_id(self, file_attachments_api_async, test_run): await file_attachments_api_async.delete(file_attachments=file_attachment.id_) # Verify it's deleted by attempting to get it (should raise error) - try: + with pytest.raises(Exception): await file_attachments_api_async.get(file_attachment_id=file_attachment.id_) - pytest.fail("Expected file attachment to be deleted") - except Exception: - pass # Expected - file was deleted finally: if os.path.exists(tmp_path): os.unlink(tmp_path) @@ -371,11 +365,8 @@ async def test_delete_single_by_object(self, file_attachments_api_async, test_ru await file_attachments_api_async.delete(file_attachments=file_attachment) # Verify it's deleted by attempting to get it (should raise error) - try: + with pytest.raises(Exception): await file_attachments_api_async.get(file_attachment_id=file_attachment.id_) - pytest.fail("Expected file attachment to be deleted") - except Exception: - pass # Expected - file was deleted finally: if os.path.exists(tmp_path): os.unlink(tmp_path) @@ -405,11 +396,8 @@ async def test_delete_multiple(self, file_attachments_api_async, test_run): # Verify they're all deleted for fa in file_attachments: - try: + with pytest.raises(Exception): await file_attachments_api_async.get(file_attachment_id=fa.id_) - pytest.fail(f"Expected file attachment {fa.id_} to be deleted") - except Exception: # noqa: PERF203 - pass # Expected - file was deleted finally: for tmp_path in tmp_paths: if os.path.exists(tmp_path): @@ -441,11 +429,8 @@ async def test_delete_list_of_ids(self, file_attachments_api_async, test_run): # Verify they're all deleted for fa_id in ids: - try: + with pytest.raises(Exception): await file_attachments_api_async.get(file_attachment_id=fa_id) - pytest.fail(f"Expected file attachment {fa_id} to be deleted") - except Exception: # noqa: PERF203 - pass # Expected - file was deleted finally: for tmp_path in tmp_paths: if os.path.exists(tmp_path): @@ -478,6 +463,19 @@ async def test_get_download_url_by_id( assert isinstance(url, str) assert len(url) > 0 + # @pytest.mark.asyncio + # async def test_download_file(self, file_attachments_api_async, uploaded_file_attachment): + # """Test downloading a file attachment.""" + # with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: + # tmp.write("Test file content\n") + # tmp_path = tmp.name + + # await file_attachments_api_async.download(file_attachment=uploaded_file_attachment, output_path=tmp_path) + # assert os.path.exists(tmp_path) + # with open(tmp_path, "r") as f: + # assert f.read() == "Test file content\n" + # os.unlink(tmp_path) + class TestFileAttachmentsAPISync: """Test suite for the synchronous File Attachments API functionality. diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index d7210ac12..b855f72c9 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset - from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate + from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate, RemoteFileEntityType from sift_client.sift_types.run import Run from sift_client.sift_types.test_report import TestReport @@ -32,6 +32,7 @@ def __init__(self, sift_client: SiftClient): super().__init__(sift_client) self._low_level_client = RemoteFilesLowLevelClient(grpc_client=self.client.grpc_client) self._upload_client = UploadLowLevelClient(rest_client=self.client.rest_client) + self.greeting = "Hello, World!" async def get(self, *, file_attachment_id: str) -> FileAttachment: """Get a file attachment by ID. @@ -54,7 +55,7 @@ async def list_( entity: Run | Asset | TestReport | None = None, remote_file_id: str | None = None, file_name: str | None = None, - entity_type: str | None = None, + entity_type: RemoteFileEntityType | None = None, entity_id: str | None = None, order_by: str | None = None, limit: int | None = None, @@ -205,7 +206,6 @@ async def upload( Args: path: The path to the file to upload. entity: The entity that the file is attached to. - entity_type: The type of entity (e.g., "runs", "annotations", "annotation_logs"). metadata: Optional metadata for the file (e.g., video/image metadata). description: Optional description of the file. organization_id: Optional organization ID. diff --git a/python/lib/sift_client/sift_types/file_attachment.py b/python/lib/sift_client/sift_types/file_attachment.py index a9fd0e5a5..eea6efb61 100644 --- a/python/lib/sift_client/sift_types/file_attachment.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -65,7 +65,7 @@ class FileAttachment(BaseType[RemoteFileProto, "FileAttachment"]): file_mime_type: str file_content_encoding: str storage_key: str - file_size: int + file_size: int # Bytes description: str created_by_user_id: str modified_by_user_id: str @@ -91,6 +91,7 @@ def _from_proto( modified_by_user_id=proto.modified_by_user_id, created_date=proto.created_date.ToDatetime(tzinfo=timezone.utc), modified_date=proto.modified_date.ToDatetime(tzinfo=timezone.utc), + proto=proto, _client=sift_client, ) From 7d5633a8006a10317f167603074207d883970e85 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 18 Nov 2025 15:47:46 -0800 Subject: [PATCH 107/127] small fixes --- .../_tests/resources/test_file_attachments.py | 10 +++++----- python/lib/sift_client/resources/file_attachments.py | 6 +++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index 1fffc4271..f618a3beb 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -197,7 +197,7 @@ async def test_get_by_id(self, file_attachments_api_async, uploaded_file_attachm async def test_get_nonexistent_raises_error(self, file_attachments_api_async): """Test that getting a non-existent file attachment raises an error.""" # Should raise an error for non-existent file attachment - with pytest.raises(Exception): + with pytest.raises(Exception, match="invalid input syntax for type uuid"): await file_attachments_api_async.get(file_attachment_id="nonexistent-file-id-12345") class TestList: @@ -340,7 +340,7 @@ async def test_delete_single_by_id(self, file_attachments_api_async, test_run): await file_attachments_api_async.delete(file_attachments=file_attachment.id_) # Verify it's deleted by attempting to get it (should raise error) - with pytest.raises(Exception): + with pytest.raises(Exception, match="An error occurred"): await file_attachments_api_async.get(file_attachment_id=file_attachment.id_) finally: if os.path.exists(tmp_path): @@ -365,7 +365,7 @@ async def test_delete_single_by_object(self, file_attachments_api_async, test_ru await file_attachments_api_async.delete(file_attachments=file_attachment) # Verify it's deleted by attempting to get it (should raise error) - with pytest.raises(Exception): + with pytest.raises(Exception, match="An error occurred"): await file_attachments_api_async.get(file_attachment_id=file_attachment.id_) finally: if os.path.exists(tmp_path): @@ -396,7 +396,7 @@ async def test_delete_multiple(self, file_attachments_api_async, test_run): # Verify they're all deleted for fa in file_attachments: - with pytest.raises(Exception): + with pytest.raises(Exception, match="An error occurred"): await file_attachments_api_async.get(file_attachment_id=fa.id_) finally: for tmp_path in tmp_paths: @@ -429,7 +429,7 @@ async def test_delete_list_of_ids(self, file_attachments_api_async, test_run): # Verify they're all deleted for fa_id in ids: - with pytest.raises(Exception): + with pytest.raises(Exception, match="An error occurred"): await file_attachments_api_async.get(file_attachment_id=fa_id) finally: for tmp_path in tmp_paths: diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index b855f72c9..6b529a002 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -11,7 +11,11 @@ if TYPE_CHECKING: from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset - from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate, RemoteFileEntityType + from sift_client.sift_types.file_attachment import ( + FileAttachment, + FileAttachmentUpdate, + RemoteFileEntityType, + ) from sift_client.sift_types.run import Run from sift_client.sift_types.test_report import TestReport From a0c6b76ba7b3e3cc0ec570fb22440ba9ef6247a0 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 18 Nov 2025 15:49:39 -0800 Subject: [PATCH 108/127] format --- python/lib/sift_client/sift_types/file_attachment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/file_attachment.py b/python/lib/sift_client/sift_types/file_attachment.py index eea6efb61..7f665bd61 100644 --- a/python/lib/sift_client/sift_types/file_attachment.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -65,7 +65,7 @@ class FileAttachment(BaseType[RemoteFileProto, "FileAttachment"]): file_mime_type: str file_content_encoding: str storage_key: str - file_size: int # Bytes + file_size: int # Bytes description: str created_by_user_id: str modified_by_user_id: str From 7ff21879521319cceb3f9f103acc06a2d1784f29 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 18 Nov 2025 16:04:15 -0800 Subject: [PATCH 109/127] update stubs --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 48aaae179..690252ab8 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -21,7 +21,11 @@ if TYPE_CHECKING: CalculatedChannelUpdate, ) from sift_client.sift_types.channel import Channel - from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate + from sift_client.sift_types.file_attachment import ( + FileAttachment, + FileAttachmentUpdate, + RemoteFileEntityType, + ) from sift_client.sift_types.report import Report, ReportUpdate from sift_client.sift_types.rule import Rule, RuleCreate, RuleUpdate from sift_client.sift_types.run import Run, RunCreate, RunUpdate @@ -572,7 +576,7 @@ class FileAttachmentsAPI: entity: Run | Asset | TestReport | None = None, remote_file_id: str | None = None, file_name: str | None = None, - entity_type: str | None = None, + entity_type: RemoteFileEntityType | None = None, entity_id: str | None = None, order_by: str | None = None, limit: int | None = None, @@ -620,7 +624,6 @@ class FileAttachmentsAPI: Args: path: The path to the file to upload. entity: The entity that the file is attached to. - entity_type: The type of entity (e.g., "runs", "annotations", "annotation_logs"). metadata: Optional metadata for the file (e.g., video/image metadata). description: Optional description of the file. organization_id: Optional organization ID. From d493fd254b89f768555b8648e9fa4b6b80d5cff4 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 19 Nov 2025 14:21:12 -0800 Subject: [PATCH 110/127] add test for downloading remote file --- .../_tests/resources/test_file_attachments.py | 39 ++++++++++++------- .../sift_client/sift_types/file_attachment.py | 3 +- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index f618a3beb..f70b5f9b8 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -463,19 +463,6 @@ async def test_get_download_url_by_id( assert isinstance(url, str) assert len(url) > 0 - # @pytest.mark.asyncio - # async def test_download_file(self, file_attachments_api_async, uploaded_file_attachment): - # """Test downloading a file attachment.""" - # with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: - # tmp.write("Test file content\n") - # tmp_path = tmp.name - - # await file_attachments_api_async.download(file_attachment=uploaded_file_attachment, output_path=tmp_path) - # assert os.path.exists(tmp_path) - # with open(tmp_path, "r") as f: - # assert f.read() == "Test file content\n" - # os.unlink(tmp_path) - class TestFileAttachmentsAPISync: """Test suite for the synchronous File Attachments API functionality. @@ -500,13 +487,18 @@ def test_basic_list(self, file_attachments_api_sync): class TestUpload: """Tests for the sync upload method.""" - def test_upload_and_delete(self, file_attachments_api_sync, test_run): - """Test synchronous upload and cleanup.""" + def test_upload_download_and_delete(self, file_attachments_api_sync, test_run): + """Test synchronous upload, download, and cleanup.""" # Create a temporary test file + completed = False with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: tmp.write("Sync test file\n") tmp_path = tmp.name + # Create a temporary download path + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as tmp: + download_path = tmp.name + try: # Upload using sync API file_attachment = file_attachments_api_sync.upload( @@ -515,11 +507,28 @@ def test_upload_and_delete(self, file_attachments_api_sync, test_run): description="Sync upload test", ) + # Verify the upload assert isinstance(file_attachment, FileAttachment) assert file_attachment.id_ is not None + # Download the file + file_attachments_api_sync.download( + file_attachment=file_attachment, + output_path=download_path + ) + + # Verify the downloaded content matches the original + with open(download_path, "r") as f: + downloaded_content = f.read() + + assert downloaded_content == "Sync test file\n" + # Cleanup file_attachments_api_sync.delete(file_attachments=file_attachment) + completed = True finally: if os.path.exists(tmp_path): os.unlink(tmp_path) + if os.path.exists(download_path): + os.unlink(download_path) + assert completed \ No newline at end of file diff --git a/python/lib/sift_client/sift_types/file_attachment.py b/python/lib/sift_client/sift_types/file_attachment.py index 7f665bd61..8c7cdb852 100644 --- a/python/lib/sift_client/sift_types/file_attachment.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -65,7 +65,8 @@ class FileAttachment(BaseType[RemoteFileProto, "FileAttachment"]): file_mime_type: str file_content_encoding: str storage_key: str - file_size: int # Bytes + file_size: int + """Size of the file in bytes.""" description: str created_by_user_id: str modified_by_user_id: str From a333d9379dd794eb922e7f3e763fe8fad5021819 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 19 Nov 2025 14:24:38 -0800 Subject: [PATCH 111/127] format --- .../lib/sift_client/_tests/resources/test_file_attachments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index f70b5f9b8..b939a485e 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -518,7 +518,7 @@ def test_upload_download_and_delete(self, file_attachments_api_sync, test_run): ) # Verify the downloaded content matches the original - with open(download_path, "r") as f: + with open(download_path) as f: downloaded_content = f.read() assert downloaded_content == "Sync test file\n" @@ -531,4 +531,4 @@ def test_upload_download_and_delete(self, file_attachments_api_sync, test_run): os.unlink(tmp_path) if os.path.exists(download_path): os.unlink(download_path) - assert completed \ No newline at end of file + assert completed From aef5177f94845da719449443c11a9ae50b651094 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Wed, 19 Nov 2025 14:29:45 -0800 Subject: [PATCH 112/127] format --- .../lib/sift_client/_tests/resources/test_file_attachments.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index b939a485e..5b3cc1006 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -513,8 +513,7 @@ def test_upload_download_and_delete(self, file_attachments_api_sync, test_run): # Download the file file_attachments_api_sync.download( - file_attachment=file_attachment, - output_path=download_path + file_attachment=file_attachment, output_path=download_path ) # Verify the downloaded content matches the original From 4b2283ceec92d0df921cd40cf33cc7d46f47cd6b Mon Sep 17 00:00:00 2001 From: Alex Luck Date: Thu, 20 Nov 2025 11:46:08 -0800 Subject: [PATCH 113/127] list_ refactor and docs fix (#393) --- .../sift_client/resources/file_attachments.py | 89 +++++++++++-------- .../sift_client/sift_types/file_attachment.py | 2 +- python/mkdocs.yml | 1 + 3 files changed, 55 insertions(+), 37 deletions(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 6b529a002..b34913d81 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -9,6 +9,8 @@ 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.asset import Asset from sift_client.sift_types.file_attachment import ( @@ -56,45 +58,62 @@ async def get(self, *, file_attachment_id: str) -> FileAttachment: async def list_( self, *, - entity: Run | Asset | TestReport | None = None, - remote_file_id: str | None = None, - file_name: str | None = None, - entity_type: RemoteFileEntityType | None = None, - entity_id: str | None = None, + name: str | None = None, + names: list[str] | None = None, + name_contains: str | None = None, + name_regex: str | re.Pattern | None = None, + # self ids + remote_file_ids: list[str] | None = None, + # created/modified ranges TODO: please make a ticket since the backend needs to add + # created_after: datetime | None = None, + # created_before: datetime | None = None, + # modified_after: datetime | None = None, + # modified_before: datetime | None = None, + # created/modified users TODO: please make a ticket since the backend needs to add + # created_by: Any | str | None = None, + # modified_by: Any | str | None = None, + # metadata TODO: please make a ticket + # metadata: list[Any] | None = None, + # file specific + entities: list[Run | Asset | TestReport] | None = None, + entity_types: list[RemoteFileEntityType] | None = None, + entity_ids: list[str] | None = None, + # common filters + description_contains: str | None = None, + filter_query: str | None = None, order_by: str | None = None, limit: int | None = None, - page_size: int | None = None, ) -> list[FileAttachment]: """List file attachments with optional filtering. - - Args: - entity: Filter by entity (Run, Asset, or TestReport). - remote_file_id: Filter by remote file ID. - file_name: Filter by file name. - entity_type: Filter by entity type enum value (e.g., 1 for Run, 3 for Asset, 5 for TestReport). - entity_id: Filter by entity ID. - order_by: The field to order by. - limit: Maximum number of results to return. - page_size: Number of results per page. - - Returns: - A list of FileAttachments. + ... """ - # Build filter parts - filter_parts = [] - - if entity is not None: - filter_parts.append(cel.equals("entity_id", entity._id_or_error)) - filter_parts.append(cel.equals("entity_type", entity._get_entity_type_name())) - else: - if entity_id: - filter_parts.append(cel.equals("entity_id", entity_id)) - if entity_type: - filter_parts.append(cel.equals("entity_type", entity_type)) - if remote_file_id: - filter_parts.append(cel.equals("remote_file_id", remote_file_id)) - if file_name: - filter_parts.append(cel.equals("file_name", file_name)) + filter_parts = [ + *self._build_name_cel_filters( + name=name, names=names, name_contains=name_contains, name_regex=name_regex + ), + # *self._build_time_cel_filters( + # created_after=created_after, + # created_before=created_before, + # modified_after=modified_after, + # modified_before=modified_before, + # created_by=created_by, + # modified_by=modified_by, + # ), + # *self._build_tags_metadata_cel_filters(metadata=metadata), + *self._build_common_cel_filters( + description_contains=description_contains, + filter_query=filter_query, + ), + ] + + entity_ids += [entity._id_or_error for entity in entities] + + if entity_ids: + filter_parts.append(cel.in_("entity_id", entity_ids)) + if entity_types: + filter_parts.append(cel.in_("entity_type", [str(et) for et in entity_types])) + if remote_file_ids: + filter_parts.append(cel.in_("remote_file_id", remote_file_ids)) query_filter = cel.and_(*filter_parts) @@ -102,8 +121,6 @@ async def list_( query_filter=query_filter or None, order_by=order_by, max_results=limit, - page_size=page_size, - sift_client=self.client, ) return self._apply_client_to_instances(file_attachments) diff --git a/python/lib/sift_client/sift_types/file_attachment.py b/python/lib/sift_client/sift_types/file_attachment.py index 8c7cdb852..f10b2fc8a 100644 --- a/python/lib/sift_client/sift_types/file_attachment.py +++ b/python/lib/sift_client/sift_types/file_attachment.py @@ -39,7 +39,7 @@ def from_str(cls, val: str) -> RemoteFileEntityType | None: return cls(int(val)) def __str__(self) -> str: - return self.name.lower() + return "ENTITY_TYPE_" + self.name @staticmethod def from_api_format(val: str) -> RemoteFileEntityType | None: diff --git a/python/mkdocs.yml b/python/mkdocs.yml index ed73da187..3129fcad3 100644 --- a/python/mkdocs.yml +++ b/python/mkdocs.yml @@ -98,6 +98,7 @@ plugins: show_symbol_type_heading: true show_symbol_type_toc: true summary: true + inherited_members: true # Custom Griffe extension to inspect the sync stubs and generate their signatures extensions: - griffe_extensions/sync_stubs_inspector.py:InspectSpecificObjects: From 2d28ff79b0d23313acf2b74cebe2157911672bdf Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 20 Nov 2025 11:56:50 -0800 Subject: [PATCH 114/127] cleanup --- python/lib/sift_client/resources/file_attachments.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index b34913d81..d0d12bf80 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -106,7 +106,10 @@ async def list_( ), ] - entity_ids += [entity._id_or_error for entity in entities] + if not entity_ids: + entity_ids = [] + if entities: + entity_ids += [entity._id_or_error for entity in entities] if entity_ids: filter_parts.append(cel.in_("entity_id", entity_ids)) From deb5d68d043d95843076c406b3fb260e63adfd32 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 20 Nov 2025 14:04:59 -0800 Subject: [PATCH 115/127] remote_files download, update cel filters --- .../low_level_wrappers/remote_files.py | 22 ++++++++ .../_tests/resources/test_file_attachments.py | 54 ++++++++++++++++--- .../sift_client/resources/file_attachments.py | 27 ++++++++-- 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 6db91ab8d..bab8f48a8 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -1,7 +1,10 @@ from __future__ import annotations +import asyncio from typing import TYPE_CHECKING, Any, cast +import requests + from sift.remote_files.v1.remote_files_pb2 import ( BatchDeleteRemoteFilesRequest, DeleteRemoteFileRequest, @@ -177,3 +180,22 @@ async def get_remote_file_download_url(self, remote_file_id: str) -> str: request ) return response.download_url + + async def download_remote_file(self, file_attachment: FileAttachment) -> bytes: + """Download a remote file. + + Args: + file_attachment: The FileAttachment to download. + + Returns: + The downloaded file. + """ + url = await self.get_remote_file_download_url(file_attachment.id_) + + # Run the synchronous requests.get in a thread pool to avoid blocking + def _download(): + response = requests.get(url) + response.raise_for_status() + return response.content + + return await asyncio.to_thread(_download) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index 5b3cc1006..ecc7eef6c 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -16,7 +16,11 @@ from sift_client import SiftClient from sift_client.resources import FileAttachmentsAPI, FileAttachmentsAPIAsync -from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate +from sift_client.sift_types.file_attachment import ( + FileAttachment, + FileAttachmentUpdate, + RemoteFileEntityType, +) pytestmark = pytest.mark.integration @@ -223,8 +227,8 @@ async def test_list_by_entity( ): """Test listing file attachments filtered by entity.""" file_attachments = await file_attachments_api_async.list_( - entity=test_run, - limit=10, + entities=[test_run], + limit=100, ) assert isinstance(file_attachments, list) @@ -243,15 +247,51 @@ async def test_list_by_entity_id( ): """Test listing file attachments filtered by entity_id.""" file_attachments = await file_attachments_api_async.list_( - entity_id=test_run.id_, - limit=10, + entity_ids=[test_run.id_], + limit=100, ) assert isinstance(file_attachments, list) # Should find our uploaded file found = any(fa.id_ == uploaded_file_attachment.id_ for fa in file_attachments) - assert found + assert found, "Uploaded file attachment not found in entity list" + + @pytest.mark.asyncio + async def test_list_by_entity_type( + self, file_attachments_api_async, uploaded_file_attachment, test_run + ): + """Test listing file attachments filtered by entity_type.""" + # Test filtering by RUNS entity type + file_attachments = await file_attachments_api_async.list_( + entity_types=[RemoteFileEntityType.RUNS], + limit=100, + ) + assert len(file_attachments) == 5 + assert isinstance(file_attachments, list) + # All returned attachments should be for RUNS + for fa in file_attachments: + assert fa.entity_type == RemoteFileEntityType.RUNS + + # Test filtering by ASSETS entity type + file_attachments = await file_attachments_api_async.list_( + entity_types=[RemoteFileEntityType.ASSETS], + limit=100, + ) + assert isinstance(file_attachments, list) + # All returned attachments should be for ASSETS + for fa in file_attachments: + assert fa.entity_type == RemoteFileEntityType.ASSETS + + # Test filtering by TEST_REPORTS entity type + file_attachments = await file_attachments_api_async.list_( + entity_types=[RemoteFileEntityType.TEST_REPORTS], + limit=100, + ) + assert isinstance(file_attachments, list) + # All returned attachments should be for TEST_REPORTS + for fa in file_attachments: + assert fa.entity_type == RemoteFileEntityType.TEST_REPORTS @pytest.mark.asyncio async def test_list_by_file_name( @@ -259,7 +299,7 @@ async def test_list_by_file_name( ): """Test listing file attachments filtered by file name.""" file_attachments = await file_attachments_api_async.list_( - file_name=uploaded_file_attachment.file_name, + names=[uploaded_file_attachment.file_name], ) assert isinstance(file_attachments, list) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index d0d12bf80..d33ee0a75 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -38,7 +38,26 @@ def __init__(self, sift_client: SiftClient): super().__init__(sift_client) self._low_level_client = RemoteFilesLowLevelClient(grpc_client=self.client.grpc_client) self._upload_client = UploadLowLevelClient(rest_client=self.client.rest_client) - self.greeting = "Hello, World!" + + def _build_name_cel_filters( + self, + *, + name: str | None = None, + names: list[str] | None = None, + name_contains: str | None = None, + name_regex: str | re.Pattern | None = None, + ) -> list[str]: + """Override base implementation to use 'file_name' field instead of 'name'.""" + filter_parts = [] + if name: + filter_parts.append(cel.equals("file_name", name)) + if names: + filter_parts.append(cel.in_("file_name", names)) + if name_contains: + filter_parts.append(cel.contains("file_name", name_contains)) + if name_regex: + filter_parts.append(cel.match("file_name", name_regex)) + return filter_parts async def get(self, *, file_attachment_id: str) -> FileAttachment: """Get a file attachment by ID. @@ -211,10 +230,10 @@ async def download( file_attachment: The FileAttachment or the ID of the file attachment to download. output_path: The path to download the file attachment to. """ - from sift_py.file_attachment._internal.download import download_remote_file - download_url = await self.get_download_url(file_attachment=file_attachment) - download_remote_file(download_url, Path(output_path)) + content = await self._low_level_client.download_remote_file(file_attachment=file_attachment) + with open(output_path, "wb") as f: + f.write(content) async def upload( self, From 3d5167cdc11fa047d1dbe7d924cce474c95f57fc Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 20 Nov 2025 14:06:59 -0800 Subject: [PATCH 116/127] format --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 1 - python/lib/sift_client/resources/file_attachments.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index bab8f48a8..ec4813236 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Any, cast import requests - from sift.remote_files.v1.remote_files_pb2 import ( BatchDeleteRemoteFilesRequest, DeleteRemoteFileRequest, diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index d33ee0a75..26ed7555c 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -1,6 +1,5 @@ from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING, Any from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient @@ -10,6 +9,7 @@ if TYPE_CHECKING: import re + from pathlib import Path from sift_client.client import SiftClient from sift_client.sift_types.asset import Asset @@ -230,7 +230,6 @@ async def download( file_attachment: The FileAttachment or the ID of the file attachment to download. output_path: The path to download the file attachment to. """ - content = await self._low_level_client.download_remote_file(file_attachment=file_attachment) with open(output_path, "wb") as f: f.write(content) From 16dd14630a0ccc35b5ea24fea3394e1941ce3394 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 20 Nov 2025 14:15:29 -0800 Subject: [PATCH 117/127] format --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 2 +- python/lib/sift_client/resources/file_attachments.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index ec4813236..95cb07de8 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -189,7 +189,7 @@ async def download_remote_file(self, file_attachment: FileAttachment) -> bytes: Returns: The downloaded file. """ - url = await self.get_remote_file_download_url(file_attachment.id_) + url = await self.get_remote_file_download_url(file_attachment._id_or_error) # Run the synchronous requests.get in a thread pool to avoid blocking def _download(): diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 26ed7555c..d4ce09109 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -230,6 +230,8 @@ async def download( file_attachment: The FileAttachment or the ID of the file attachment to download. output_path: The path to download the file attachment to. """ + if isinstance(file_attachment, str): + file_attachment = await self.get(file_attachment_id=file_attachment) content = await self._low_level_client.download_remote_file(file_attachment=file_attachment) with open(output_path, "wb") as f: f.write(content) From bb9e731b88ed32f9cdaaa0da1ce1a61829b27da7 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 20 Nov 2025 14:37:29 -0800 Subject: [PATCH 118/127] format --- .../low_level_wrappers/remote_files.py | 8 +---- .../sift_client/resources/file_attachments.py | 4 ++- .../resources/sync_stubs/__init__.pyi | 31 ++++++++----------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 95cb07de8..150a0e772 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -63,7 +63,6 @@ async def get_remote_file( async def list_all_remote_files( self, query_filter: str | None = None, - order_by: str | None = None, max_results: int | None = None, page_size: int | None = None, sift_client: SiftClient | None = None, @@ -72,7 +71,7 @@ async def list_all_remote_files( Args: query_filter: The CEL query filter. - order_by: The field to order by. + max_results: The maximum number of results to return. page_size: The number of results to return per page. sift_client: The SiftClient to attach to the returned RemoteFiles. @@ -84,7 +83,6 @@ async def list_all_remote_files( self.list_remote_files, kwargs={"query_filter": query_filter, "sift_client": sift_client}, page_size=page_size, - order_by=order_by, max_results=max_results, ) @@ -93,7 +91,6 @@ async def list_remote_files( page_size: int | None = None, page_token: str | None = None, query_filter: str | None = None, - order_by: str | None = None, sift_client: SiftClient | None = None, ) -> tuple[list[FileAttachment], str]: """List remote files with pagination support. @@ -102,7 +99,6 @@ async def list_remote_files( page_size: The number of results to return per page. page_token: The page token for pagination. query_filter: The CEL query filter. - order_by: The field to order by. sift_client: The SiftClient to attach to the returned RemoteFiles. Returns: @@ -117,8 +113,6 @@ async def list_remote_files( request_kwargs["page_token"] = page_token if query_filter is not None: request_kwargs["filter"] = query_filter - if order_by is not None: - request_kwargs["order_by"] = order_by request = ListRemoteFilesRequest(**request_kwargs) response = await self._grpc_client.get_stub(RemoteFileServiceStub).ListRemoteFiles(request) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index d4ce09109..a8a9bd918 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -104,6 +104,9 @@ async def list_( limit: int | None = None, ) -> list[FileAttachment]: """List file attachments with optional filtering. + + Note: + order_by is accepted for API consistency but not currently supported by the backend. ... """ filter_parts = [ @@ -141,7 +144,6 @@ async def list_( file_attachments = await self._low_level_client.list_all_remote_files( query_filter=query_filter or None, - order_by=order_by, max_results=limit, ) return self._apply_client_to_instances(file_attachments) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 690252ab8..1510cb52f 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -573,29 +573,24 @@ class FileAttachmentsAPI: def list_( self, *, - entity: Run | Asset | TestReport | None = None, - remote_file_id: str | None = None, - file_name: str | None = None, - entity_type: RemoteFileEntityType | None = None, - entity_id: str | None = None, + name: str | None = None, + names: list[str] | None = None, + name_contains: str | None = None, + name_regex: str | re.Pattern | None = None, + remote_file_ids: list[str] | None = None, + entities: list[Run | Asset | TestReport] | None = None, + entity_types: list[RemoteFileEntityType] | None = None, + entity_ids: list[str] | None = None, + description_contains: str | None = None, + filter_query: str | None = None, order_by: str | None = None, limit: int | None = None, - page_size: int | None = None, ) -> list[FileAttachment]: """List file attachments with optional filtering. - Args: - entity: Filter by entity (Run, Asset, or TestReport). - remote_file_id: Filter by remote file ID. - file_name: Filter by file name. - entity_type: Filter by entity type enum value (e.g., 1 for Run, 3 for Asset, 5 for TestReport). - entity_id: Filter by entity ID. - order_by: The field to order by. - limit: Maximum number of results to return. - page_size: Number of results per page. - - Returns: - A list of FileAttachments. + Note: + order_by is accepted for API consistency but not currently supported by the backend. + ... """ ... From d09f6d07c1d24d585d0b9f40e51bf46132172a9e Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 20 Nov 2025 14:42:09 -0800 Subject: [PATCH 119/127] cleanup --- python/lib/sift_client/sift_types/_mixins/file_attachments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/sift_types/_mixins/file_attachments.py b/python/lib/sift_client/sift_types/_mixins/file_attachments.py index 39b0d5166..304b03c6a 100644 --- a/python/lib/sift_client/sift_types/_mixins/file_attachments.py +++ b/python/lib/sift_client/sift_types/_mixins/file_attachments.py @@ -73,7 +73,7 @@ def attachments(self: _SupportsFileAttachments) -> list[FileAttachment]: if not isinstance(self, (Asset, Run, TestReport)): raise ValueError("Entity is not a valid entity type") return self.client.file_attachments.list_( - entity=self, + entities=[self], ) def delete_attachment( From bc1179ee5b20646cf8ed902abcfc47c911987fa7 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 20 Nov 2025 14:50:20 -0800 Subject: [PATCH 120/127] test fixes --- .../_tests/sift_types/test_asset.py | 2 +- .../_tests/sift_types/test_results.py | 2 +- .../sift_client/_tests/sift_types/test_run.py | 23 +------------------ 3 files changed, 3 insertions(+), 24 deletions(-) diff --git a/python/lib/sift_client/_tests/sift_types/test_asset.py b/python/lib/sift_client/_tests/sift_types/test_asset.py index d95fda260..23d49970d 100644 --- a/python/lib/sift_client/_tests/sift_types/test_asset.py +++ b/python/lib/sift_client/_tests/sift_types/test_asset.py @@ -179,7 +179,7 @@ def test_attachments_property_fetches_files(self, mock_asset, mock_client): # Verify file_attachments.list_ was called with correct parameters mock_client.file_attachments.list_.assert_called_once_with( - entity=mock_asset, + entities=[mock_asset], ) # Verify result diff --git a/python/lib/sift_client/_tests/sift_types/test_results.py b/python/lib/sift_client/_tests/sift_types/test_results.py index ac9d8309c..3fcd694e0 100644 --- a/python/lib/sift_client/_tests/sift_types/test_results.py +++ b/python/lib/sift_client/_tests/sift_types/test_results.py @@ -225,7 +225,7 @@ def test_attachments_property_fetches_files(self, mock_test_report, mock_client) # Verify file_attachments.list_ was called with correct parameters mock_client.file_attachments.list_.assert_called_once_with( - entity=mock_test_report, + entities=[mock_test_report], ) # Verify result diff --git a/python/lib/sift_client/_tests/sift_types/test_run.py b/python/lib/sift_client/_tests/sift_types/test_run.py index fa2ea3207..64c6b8cad 100644 --- a/python/lib/sift_client/_tests/sift_types/test_run.py +++ b/python/lib/sift_client/_tests/sift_types/test_run.py @@ -206,27 +206,6 @@ def test_update_calls_client_and_updates_self(self, mock_run, mock_client): # Verify it returns self assert result is mock_run - def test_remote_files_property_fetches_files(self, mock_run, mock_client): - """Test that attachments property fetches files from client.file_attachments API.""" - # Create mock remote files - mock_remote_file = MagicMock() - mock_remote_file.entity_id = mock_run.id_ - mock_remote_files = [mock_remote_file] - - # Mock the file_attachments API - mock_client.file_attachments.list_.return_value = mock_remote_files - - # Access the attachments property (it's a property, not a method) - result = mock_run.attachments - - # Verify file_attachments.list_ was called with correct parameters - mock_client.file_attachments.list_.assert_called_once_with( - entity=mock_run, - ) - - # Verify result - assert result == mock_remote_files - def test_attachments_property_fetches_files(self, mock_run, mock_client): """Test that attachments property fetches files from client.file_attachments API.""" # Create mock remote files @@ -242,7 +221,7 @@ def test_attachments_property_fetches_files(self, mock_run, mock_client): # Verify file_attachments.list_ was called with correct parameters mock_client.file_attachments.list_.assert_called_once_with( - entity=mock_run, + entities=[mock_run], ) # Verify result From 80eee3f01089ad585fdb6057ef455400f286230a Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 20 Nov 2025 15:05:00 -0800 Subject: [PATCH 121/127] add order by back in --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 150a0e772..7d3c01624 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -65,6 +65,7 @@ async def list_all_remote_files( query_filter: str | None = None, max_results: int | None = None, page_size: int | None = None, + order_by: str | None = None, sift_client: SiftClient | None = None, ) -> list[FileAttachment]: """List all remote files matching the given query. @@ -74,6 +75,7 @@ async def list_all_remote_files( max_results: The maximum number of results to return. page_size: The number of results to return per page. + order_by: The field to order by. Not supported by the backend so it is ignored. sift_client: The SiftClient to attach to the returned RemoteFiles. Returns: @@ -84,6 +86,7 @@ async def list_all_remote_files( kwargs={"query_filter": query_filter, "sift_client": sift_client}, page_size=page_size, max_results=max_results, + order_by=order_by, ) async def list_remote_files( @@ -91,6 +94,7 @@ async def list_remote_files( page_size: int | None = None, page_token: str | None = None, query_filter: str | None = None, + order_by: str | None = None, sift_client: SiftClient | None = None, ) -> tuple[list[FileAttachment], str]: """List remote files with pagination support. @@ -99,6 +103,7 @@ async def list_remote_files( page_size: The number of results to return per page. page_token: The page token for pagination. query_filter: The CEL query filter. + order_by: The field to order by. Not supported by the backend so it is ignored. sift_client: The SiftClient to attach to the returned RemoteFiles. Returns: From 99fe7c6d3fe0bf28ac057abb28217704dd1a1250 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 20 Nov 2025 15:55:17 -0800 Subject: [PATCH 122/127] only 1 type at a time --- .../sift_client/_tests/resources/test_file_attachments.py | 7 +++---- python/lib/sift_client/resources/file_attachments.py | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/python/lib/sift_client/_tests/resources/test_file_attachments.py b/python/lib/sift_client/_tests/resources/test_file_attachments.py index ecc7eef6c..ad20af0d7 100644 --- a/python/lib/sift_client/_tests/resources/test_file_attachments.py +++ b/python/lib/sift_client/_tests/resources/test_file_attachments.py @@ -264,10 +264,9 @@ async def test_list_by_entity_type( """Test listing file attachments filtered by entity_type.""" # Test filtering by RUNS entity type file_attachments = await file_attachments_api_async.list_( - entity_types=[RemoteFileEntityType.RUNS], + entity_type=RemoteFileEntityType.RUNS, limit=100, ) - assert len(file_attachments) == 5 assert isinstance(file_attachments, list) # All returned attachments should be for RUNS for fa in file_attachments: @@ -275,7 +274,7 @@ async def test_list_by_entity_type( # Test filtering by ASSETS entity type file_attachments = await file_attachments_api_async.list_( - entity_types=[RemoteFileEntityType.ASSETS], + entity_type=RemoteFileEntityType.ASSETS, limit=100, ) assert isinstance(file_attachments, list) @@ -285,7 +284,7 @@ async def test_list_by_entity_type( # Test filtering by TEST_REPORTS entity type file_attachments = await file_attachments_api_async.list_( - entity_types=[RemoteFileEntityType.TEST_REPORTS], + entity_type=RemoteFileEntityType.TEST_REPORTS, limit=100, ) assert isinstance(file_attachments, list) diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index a8a9bd918..7bf39363e 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -95,7 +95,7 @@ async def list_( # metadata: list[Any] | None = None, # file specific entities: list[Run | Asset | TestReport] | None = None, - entity_types: list[RemoteFileEntityType] | None = None, + entity_type: RemoteFileEntityType | None = None, entity_ids: list[str] | None = None, # common filters description_contains: str | None = None, @@ -135,8 +135,8 @@ async def list_( if entity_ids: filter_parts.append(cel.in_("entity_id", entity_ids)) - if entity_types: - filter_parts.append(cel.in_("entity_type", [str(et) for et in entity_types])) + if entity_type: + filter_parts.append(cel.equals("entity_type", entity_type.name.lower())) if remote_file_ids: filter_parts.append(cel.in_("remote_file_id", remote_file_ids)) From cc1e1dbfc1af60384581ac86dfdc46581f52e7ce Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Thu, 20 Nov 2025 15:59:16 -0800 Subject: [PATCH 123/127] stub fix --- python/lib/sift_client/resources/sync_stubs/__init__.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 1510cb52f..2b5dfda2a 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -579,7 +579,7 @@ class FileAttachmentsAPI: name_regex: str | re.Pattern | None = None, remote_file_ids: list[str] | None = None, entities: list[Run | Asset | TestReport] | None = None, - entity_types: list[RemoteFileEntityType] | None = None, + entity_type: RemoteFileEntityType | None = None, entity_ids: list[str] | None = None, description_contains: str | None = None, filter_query: str | None = None, From f5b0e80d5cb719f176d1c4a67e7b7fc6ea0b9346 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Mon, 24 Nov 2025 09:00:49 -0800 Subject: [PATCH 124/127] small asyncio change --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 7d3c01624..5c00476c2 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -196,4 +196,6 @@ def _download(): response.raise_for_status() return response.content - return await asyncio.to_thread(_download) + # Use run_in_executor for Python 3.8 compatibility (asyncio.to_thread was added in 3.9) + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _download) From f971341ff7cbd92f6fa49a176d5e3a07c08ad9a4 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 25 Nov 2025 09:39:18 -0800 Subject: [PATCH 125/127] update docstrings and include TODOs --- .../low_level_wrappers/remote_files.py | 7 ++++-- .../sift_client/resources/file_attachments.py | 25 ++++++++++++++----- .../resources/sync_stubs/__init__.pyi | 19 +++++++++++--- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 5c00476c2..f10180527 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -75,7 +75,7 @@ async def list_all_remote_files( max_results: The maximum number of results to return. page_size: The number of results to return per page. - order_by: The field to order by. Not supported by the backend so it is ignored. + order_by: The field to order by. Not supported by the backend, but it is here for API consistency. TODO: Add to backend sift_client: The SiftClient to attach to the returned RemoteFiles. Returns: @@ -103,7 +103,7 @@ async def list_remote_files( page_size: The number of results to return per page. page_token: The page token for pagination. query_filter: The CEL query filter. - order_by: The field to order by. Not supported by the backend so it is ignored. + order_by: The field to order by. Not supported by the backend, but it is here for API consistency. TODO: Add to backend sift_client: The SiftClient to attach to the returned RemoteFiles. Returns: @@ -111,6 +111,9 @@ async def list_remote_files( """ from sift_client.sift_types.file_attachment import FileAttachment + if order_by is not None: + raise ValueError("order_by is not supported by the backend, but it is here for API consistency.") + request_kwargs: dict[str, Any] = {} if page_size is not None: request_kwargs["page_size"] = page_size diff --git a/python/lib/sift_client/resources/file_attachments.py b/python/lib/sift_client/resources/file_attachments.py index 7bf39363e..68b930e3c 100644 --- a/python/lib/sift_client/resources/file_attachments.py +++ b/python/lib/sift_client/resources/file_attachments.py @@ -83,15 +83,15 @@ async def list_( name_regex: str | re.Pattern | None = None, # self ids remote_file_ids: list[str] | None = None, - # created/modified ranges TODO: please make a ticket since the backend needs to add + # created/modified ranges TODO: Add to backend # created_after: datetime | None = None, # created_before: datetime | None = None, # modified_after: datetime | None = None, # modified_before: datetime | None = None, - # created/modified users TODO: please make a ticket since the backend needs to add + # created/modified users TODO: Add to backend # created_by: Any | str | None = None, # modified_by: Any | str | None = None, - # metadata TODO: please make a ticket + # metadata TODO: Add to backend # metadata: list[Any] | None = None, # file specific entities: list[Run | Asset | TestReport] | None = None, @@ -105,9 +105,22 @@ async def list_( ) -> list[FileAttachment]: """List file attachments with optional filtering. - Note: - order_by is accepted for API consistency but not currently supported by the backend. - ... + Args: + name: Exact name of the file attachment. + names: List of file attachment names to filter by. + name_contains: Partial name of the file attachment. + name_regex: Regular expression to filter file attachments by name. + remote_file_ids: Filter to file attachments with any of these IDs. + entities: Filter to file attachments associated with these entities. + entity_type: Filter to file attachments associated with this entity type. + entity_ids: Filter to file attachments associated with these entity IDs. + description_contains: Partial description of the file attachment. + filter_query: Explicit CEL query to filter file attachments. + order_by: Field and direction to order results by. Note: Not supported by the backend, but it is here for API consistency. + limit: Maximum number of file attachments to return. If None, returns all matches. + + Returns: + A list of FileAttachment objects that match the filter criteria. """ filter_parts = [ *self._build_name_cel_filters( diff --git a/python/lib/sift_client/resources/sync_stubs/__init__.pyi b/python/lib/sift_client/resources/sync_stubs/__init__.pyi index 2b5dfda2a..96686486c 100644 --- a/python/lib/sift_client/resources/sync_stubs/__init__.pyi +++ b/python/lib/sift_client/resources/sync_stubs/__init__.pyi @@ -588,9 +588,22 @@ class FileAttachmentsAPI: ) -> list[FileAttachment]: """List file attachments with optional filtering. - Note: - order_by is accepted for API consistency but not currently supported by the backend. - ... + Args: + name: Exact name of the file attachment. + names: List of file attachment names to filter by. + name_contains: Partial name of the file attachment. + name_regex: Regular expression to filter file attachments by name. + remote_file_ids: Filter to file attachments with any of these IDs. + entities: Filter to file attachments associated with these entities. + entity_type: Filter to file attachments associated with this entity type. + entity_ids: Filter to file attachments associated with these entity IDs. + description_contains: Partial description of the file attachment. + filter_query: Explicit CEL query to filter file attachments. + order_by: Field and direction to order results by. Note: Not supported by the backend, but it is here for API consistency. + limit: Maximum number of file attachments to return. If None, returns all matches. + + Returns: + A list of FileAttachment objects that match the filter criteria. """ ... From ad9cb59e8716e87c1932d1023fab7e711cea6322 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 25 Nov 2025 09:43:28 -0800 Subject: [PATCH 126/127] format --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index f10180527..8f869e697 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -112,7 +112,9 @@ async def list_remote_files( from sift_client.sift_types.file_attachment import FileAttachment if order_by is not None: - raise ValueError("order_by is not supported by the backend, but it is here for API consistency.") + raise ValueError( + "order_by is not supported by the backend, but it is here for API consistency." + ) request_kwargs: dict[str, Any] = {} if page_size is not None: From de952c1c4ef5eb81535e52c9db6c40e00e187127 Mon Sep 17 00:00:00 2001 From: Andrew Baker Date: Tue, 25 Nov 2025 10:52:49 -0800 Subject: [PATCH 127/127] not implemented error --- .../sift_client/_internal/low_level_wrappers/remote_files.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py index 8f869e697..b1ef49833 100644 --- a/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py +++ b/python/lib/sift_client/_internal/low_level_wrappers/remote_files.py @@ -112,9 +112,7 @@ async def list_remote_files( from sift_client.sift_types.file_attachment import FileAttachment if order_by is not None: - raise ValueError( - "order_by is not supported by the backend, but it is here for API consistency." - ) + raise NotImplementedError request_kwargs: dict[str, Any] = {} if page_size is not None: