Skip to content

Commit d57788b

Browse files
Python(feat): Remote Files API (#366)
Added Remote Files API to the python client.
1 parent 2a5d0f8 commit d57788b

21 files changed

Lines changed: 1591 additions & 7 deletions

File tree

python/lib/sift_client/_internal/low_level_wrappers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from sift_client._internal.low_level_wrappers.channels import ChannelsLowLevelClient
66
from sift_client._internal.low_level_wrappers.ingestion import IngestionLowLevelClient
77
from sift_client._internal.low_level_wrappers.ping import PingLowLevelClient
8+
from sift_client._internal.low_level_wrappers.remote_files import RemoteFilesLowLevelClient
89
from sift_client._internal.low_level_wrappers.reports import ReportsLowLevelClient
910
from sift_client._internal.low_level_wrappers.rules import RulesLowLevelClient
1011
from sift_client._internal.low_level_wrappers.runs import RunsLowLevelClient
@@ -18,6 +19,7 @@
1819
"ChannelsLowLevelClient",
1920
"IngestionLowLevelClient",
2021
"PingLowLevelClient",
22+
"RemoteFilesLowLevelClient",
2123
"ReportsLowLevelClient",
2224
"RulesLowLevelClient",
2325
"RunsLowLevelClient",
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from typing import TYPE_CHECKING, Any, cast
5+
6+
import requests
7+
from sift.remote_files.v1.remote_files_pb2 import (
8+
BatchDeleteRemoteFilesRequest,
9+
DeleteRemoteFileRequest,
10+
GetRemoteFileDownloadUrlRequest,
11+
GetRemoteFileRequest,
12+
GetRemoteFileResponse,
13+
ListRemoteFilesRequest,
14+
ListRemoteFilesResponse,
15+
UpdateRemoteFileRequest,
16+
UpdateRemoteFileResponse,
17+
)
18+
from sift.remote_files.v1.remote_files_pb2_grpc import RemoteFileServiceStub
19+
20+
from sift_client._internal.low_level_wrappers.base import (
21+
LowLevelClientBase,
22+
)
23+
from sift_client.transport import GrpcClient, WithGrpcClient
24+
25+
if TYPE_CHECKING:
26+
from sift_client.client import SiftClient
27+
from sift_client.sift_types.file_attachment import FileAttachment, FileAttachmentUpdate
28+
29+
30+
class RemoteFilesLowLevelClient(LowLevelClientBase, WithGrpcClient):
31+
"""Low-level client for the RemoteFilesAPI.
32+
33+
This class provides a thin wrapper around the autogenerated bindings for the RemoteFilesAPI.
34+
"""
35+
36+
def __init__(self, grpc_client: GrpcClient):
37+
"""Initialize the RemoteFilesLowLevelClient.
38+
39+
Args:
40+
grpc_client: The gRPC client to use for making API calls.
41+
"""
42+
super().__init__(grpc_client)
43+
44+
async def get_remote_file(
45+
self, remote_file_id: str, sift_client: SiftClient | None = None
46+
) -> FileAttachment:
47+
"""Get a remote file by ID.
48+
49+
Args:
50+
remote_file_id: The ID of the remote file to retrieve.
51+
sift_client: The SiftClient to attach to the returned RemoteFile.
52+
53+
Returns:
54+
The RemoteFile.
55+
"""
56+
from sift_client.sift_types.file_attachment import FileAttachment
57+
58+
request = GetRemoteFileRequest(remote_file_id=remote_file_id)
59+
response = await self._grpc_client.get_stub(RemoteFileServiceStub).GetRemoteFile(request)
60+
grpc_remote_file = cast("GetRemoteFileResponse", response).remote_file
61+
return FileAttachment._from_proto(grpc_remote_file, sift_client)
62+
63+
async def list_all_remote_files(
64+
self,
65+
query_filter: str | None = None,
66+
max_results: int | None = None,
67+
page_size: int | None = None,
68+
order_by: str | None = None,
69+
sift_client: SiftClient | None = None,
70+
) -> list[FileAttachment]:
71+
"""List all remote files matching the given query.
72+
73+
Args:
74+
query_filter: The CEL query filter.
75+
76+
max_results: The maximum number of results to return.
77+
page_size: The number of results to return per page.
78+
order_by: The field to order by. Not supported by the backend, but it is here for API consistency. TODO: Add to backend
79+
sift_client: The SiftClient to attach to the returned RemoteFiles.
80+
81+
Returns:
82+
A list of RemoteFiles matching the given query.
83+
"""
84+
return await self._handle_pagination(
85+
self.list_remote_files,
86+
kwargs={"query_filter": query_filter, "sift_client": sift_client},
87+
page_size=page_size,
88+
max_results=max_results,
89+
order_by=order_by,
90+
)
91+
92+
async def list_remote_files(
93+
self,
94+
page_size: int | None = None,
95+
page_token: str | None = None,
96+
query_filter: str | None = None,
97+
order_by: str | None = None,
98+
sift_client: SiftClient | None = None,
99+
) -> tuple[list[FileAttachment], str]:
100+
"""List remote files with pagination support.
101+
102+
Args:
103+
page_size: The number of results to return per page.
104+
page_token: The page token for pagination.
105+
query_filter: The CEL query filter.
106+
order_by: The field to order by. Not supported by the backend, but it is here for API consistency. TODO: Add to backend
107+
sift_client: The SiftClient to attach to the returned RemoteFiles.
108+
109+
Returns:
110+
A tuple of (list of RemoteFiles, next_page_token).
111+
"""
112+
from sift_client.sift_types.file_attachment import FileAttachment
113+
114+
if order_by is not None:
115+
raise NotImplementedError
116+
117+
request_kwargs: dict[str, Any] = {}
118+
if page_size is not None:
119+
request_kwargs["page_size"] = page_size
120+
if page_token is not None:
121+
request_kwargs["page_token"] = page_token
122+
if query_filter is not None:
123+
request_kwargs["filter"] = query_filter
124+
125+
request = ListRemoteFilesRequest(**request_kwargs)
126+
response = await self._grpc_client.get_stub(RemoteFileServiceStub).ListRemoteFiles(request)
127+
response = cast("ListRemoteFilesResponse", response)
128+
return [
129+
FileAttachment._from_proto(rf, sift_client) for rf in response.remote_files
130+
], response.next_page_token
131+
132+
async def update_remote_file(
133+
self, update: FileAttachmentUpdate, sift_client: SiftClient | None = None
134+
) -> FileAttachment:
135+
"""Update a remote file.
136+
137+
Args:
138+
update: The FileAttachmentUpdate containing the fields to update.
139+
sift_client: The SiftClient to attach to the returned RemoteFile.
140+
141+
Returns:
142+
The updated RemoteFile.
143+
"""
144+
from sift_client.sift_types.file_attachment import FileAttachment
145+
146+
grpc_remote_file, update_mask = update.to_proto_with_mask()
147+
request = UpdateRemoteFileRequest(remote_file=grpc_remote_file, update_mask=update_mask)
148+
response = await self._grpc_client.get_stub(RemoteFileServiceStub).UpdateRemoteFile(request)
149+
updated_grpc_remote_file = cast("UpdateRemoteFileResponse", response).remote_file
150+
return FileAttachment._from_proto(updated_grpc_remote_file, sift_client)
151+
152+
async def delete_remote_file(self, remote_file_id: str) -> None:
153+
"""Delete a remote file.
154+
155+
Args:
156+
remote_file_id: The ID of the remote file to delete.
157+
"""
158+
request = DeleteRemoteFileRequest(remote_file_id=remote_file_id)
159+
await self._grpc_client.get_stub(RemoteFileServiceStub).DeleteRemoteFile(request)
160+
161+
async def batch_delete_remote_files(self, remote_file_ids: list[str]) -> None:
162+
"""Batch delete remote files.
163+
164+
Args:
165+
remote_file_ids: The IDs of the remote files to delete (up to 1000).
166+
"""
167+
request = BatchDeleteRemoteFilesRequest(remote_file_ids=remote_file_ids)
168+
await self._grpc_client.get_stub(RemoteFileServiceStub).BatchDeleteRemoteFiles(request)
169+
170+
async def get_remote_file_download_url(self, remote_file_id: str) -> str:
171+
"""Get a download URL for a remote file.
172+
173+
Args:
174+
remote_file_id: The ID of the remote file.
175+
176+
Returns:
177+
The download URL for the remote file.
178+
"""
179+
request = GetRemoteFileDownloadUrlRequest(remote_file_id=remote_file_id)
180+
response = await self._grpc_client.get_stub(RemoteFileServiceStub).GetRemoteFileDownloadUrl(
181+
request
182+
)
183+
return response.download_url
184+
185+
async def download_remote_file(self, file_attachment: FileAttachment) -> bytes:
186+
"""Download a remote file.
187+
188+
Args:
189+
file_attachment: The FileAttachment to download.
190+
191+
Returns:
192+
The downloaded file.
193+
"""
194+
url = await self.get_remote_file_download_url(file_attachment._id_or_error)
195+
196+
# Run the synchronous requests.get in a thread pool to avoid blocking
197+
def _download():
198+
response = requests.get(url)
199+
response.raise_for_status()
200+
return response.content
201+
202+
# Use run_in_executor for Python 3.8 compatibility (asyncio.to_thread was added in 3.9)
203+
loop = asyncio.get_event_loop()
204+
return await loop.run_in_executor(None, _download)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Sift Python
2+
3+
## Running Integration Tests Locally
4+
5+
1. Create Environmental Variables
6+
a. Create or open a .env file in /python
7+
b. Add an API key for SIFT_API_KEY
8+
2. Start Local Sift
9+
3. Asset Data: NostromoLV426
10+
a. Ensure that your local Sift instance contains data for the asset NostromoLV426
11+
b. If it doesn't them export data for NostromoLV426 from development
12+
4. Run tests
13+
a. Run tests using /python/scripts/dev {test, test-integration, test-all}

python/lib/sift_client/_tests/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ def sift_client() -> SiftClient:
1919
grpc_url = os.getenv("SIFT_GRPC_URI", "localhost:50051")
2020
rest_url = os.getenv("SIFT_REST_URI", "localhost:8080")
2121
api_key = os.getenv("SIFT_API_KEY", "")
22+
# If the URL contains localhost, don't use SSL. Most likely running tests or local development.
23+
use_ssl = not ("localhost" in grpc_url or "localhost" in rest_url)
2224

2325
client = SiftClient(
2426
connection_config=SiftConnectionConfig(
2527
api_key=api_key,
2628
grpc_url=grpc_url,
2729
rest_url=rest_url,
28-
use_ssl=True,
30+
use_ssl=use_ssl,
2931
)
3032
)
3133

@@ -45,6 +47,7 @@ def mock_client():
4547
client.rules = MagicMock()
4648
client.tags = MagicMock()
4749
client.test_results = MagicMock()
50+
client.file_attachments = MagicMock()
4851
client.async_ = MagicMock(spec=AsyncAPIs)
4952
client.async_.ingestion = MagicMock()
5053
return client

0 commit comments

Comments
 (0)