Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions python/lib/sift_client/_internal/low_level_wrappers/remote_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions python/lib/sift_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
TestResultsAPI,
TestResultsAPIAsync,
)
from sift_client.resources.sync_stubs import FileAttachmentsAPI
from sift_client.transport import (
GrpcClient,
GrpcConfig,
Expand Down Expand Up @@ -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),
Expand All @@ -158,6 +161,7 @@ def __init__(
runs=RunsAPIAsync(self),
tags=TagsAPIAsync(self),
test_results=TestResultsAPIAsync(self),
file_attachments=FileAttachmentsAPIAsync(self),
)

@property
Expand Down
4 changes: 4 additions & 0 deletions python/lib/sift_client/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -172,6 +173,7 @@ async def main():
RunsAPI,
TagsAPI,
TestResultsAPI,
# FileAttachmentsAPI
)

__all__ = [
Expand All @@ -194,4 +196,6 @@ async def main():
"TagsAPIAsync",
"TestResultsAPI",
"TestResultsAPIAsync",
"FileAttachmentsAPIAsync",
# "FileAttachmentsAPI"
]
45 changes: 45 additions & 0 deletions python/lib/sift_client/resources/file_attachments.py
Original file line number Diff line number Diff line change
@@ -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: ...
3 changes: 3 additions & 0 deletions python/lib/sift_client/resources/sync_stubs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
AssetsAPIAsync,
CalculatedChannelsAPIAsync,
ChannelsAPIAsync,
FileAttachmentsAPIAsync,
PingAPIAsync,
ReportsAPIAsync,
RulesAPIAsync,
Expand All @@ -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",
Expand Down
Empty file.
65 changes: 65 additions & 0 deletions python/lib/sift_client/sift_types/_mixins/file_attachments.py
Original file line number Diff line number Diff line change
@@ -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,
)
36 changes: 2 additions & 34 deletions python/lib/sift_client/sift_types/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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

Expand Down
Loading
Loading