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
46 changes: 29 additions & 17 deletions src/runloop_api_client/sdk/async_.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,22 +330,25 @@ async def list(
async def upload_from_file(
self,
file_path: str | Path,
name: str | None = None,
*,
content_type: ContentType | None = None,
name: Optional[str] = None,
content_type: Optional[ContentType] = None,
metadata: Optional[Dict[str, str]] = None,
ttl: Optional[timedelta] = None,
**options: Unpack[LongRequestOptions],
) -> AsyncStorageObject:
"""Create and upload an object from a local file path.

:param file_path: Local filesystem path to read
:type file_path: str | Path
:param name: Optional object name; defaults to the file name, defaults to None
:type name: str | None, optional
:param content_type: Optional MIME type to apply to the object, defaults to None
:type content_type: ContentType | None, optional
:param metadata: Optional key-value metadata, defaults to None
:type metadata: Optional[Dict[str, str]], optional
:param name: Optional object name; defaults to the file name
:type name: Optional[str]
:param content_type: Optional MIME type to apply to the object
:type content_type: Optional[ContentType]
:param metadata: Optional key-value metadata
:type metadata: Optional[Dict[str, str]]
:param ttl: Optional Time-To-Live, after which the object is automatically deleted
:type ttl: Optional[timedelta]
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
:return: Wrapper for the uploaded object
:rtype: AsyncStorageObject
Expand All @@ -360,7 +363,8 @@ async def upload_from_file(

name = name or path.name
content_type = content_type or detect_content_type(str(file_path))
obj = await self.create(name=name, content_type=content_type, metadata=metadata, **options)
ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None
obj = await self.create(name=name, content_type=content_type, metadata=metadata, ttl_ms=ttl_ms, **options)
await obj.upload_content(content)
await obj.complete()
return obj
Expand Down Expand Up @@ -412,9 +416,10 @@ def synchronous_io() -> bytes:
async def upload_from_text(
self,
text: str,
name: str,
*,
name: str,
metadata: Optional[Dict[str, str]] = None,
ttl: Optional[timedelta] = None,
**options: Unpack[LongRequestOptions],
) -> AsyncStorageObject:
"""Create and upload an object from a text payload.
Expand All @@ -423,24 +428,28 @@ async def upload_from_text(
:type text: str
:param name: Object display name
:type name: str
:param metadata: Optional key-value metadata, defaults to None
:type metadata: Optional[Dict[str, str]], optional
:param metadata: Optional key-value metadata
:type metadata: Optional[Dict[str, str]]
:param ttl: Optional Time-To-Live, after which the object is automatically deleted
:type ttl: Optional[timedelta]
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
:return: Wrapper for the uploaded object
:rtype: AsyncStorageObject
"""
obj = await self.create(name=name, content_type="text", metadata=metadata, **options)
ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None
obj = await self.create(name=name, content_type="text", metadata=metadata, ttl_ms=ttl_ms, **options)
await obj.upload_content(text)
await obj.complete()
return obj

async def upload_from_bytes(
self,
data: bytes,
name: str,
*,
name: str,
content_type: ContentType,
metadata: Optional[Dict[str, str]] = None,
ttl: Optional[timedelta] = None,
**options: Unpack[LongRequestOptions],
) -> AsyncStorageObject:
"""Create and upload an object from a bytes payload.
Expand All @@ -451,13 +460,16 @@ async def upload_from_bytes(
:type name: str
:param content_type: MIME type describing the payload
:type content_type: ContentType
:param metadata: Optional key-value metadata, defaults to None
:type metadata: Optional[Dict[str, str]], optional
:param metadata: Optional key-value metadata
:type metadata: Optional[Dict[str, str]]
:param ttl: Optional Time-To-Live, after which the object is automatically deleted
:type ttl: Optional[timedelta]
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
:return: Wrapper for the uploaded object
:rtype: AsyncStorageObject
"""
obj = await self.create(name=name, content_type=content_type, metadata=metadata, **options)
ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None
obj = await self.create(name=name, content_type=content_type, metadata=metadata, ttl_ms=ttl_ms, **options)
await obj.upload_content(data)
await obj.complete()
return obj
Expand Down
46 changes: 29 additions & 17 deletions src/runloop_api_client/sdk/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,22 +329,25 @@ def list(
def upload_from_file(
self,
file_path: str | Path,
name: str | None = None,
*,
content_type: ContentType | None = None,
name: Optional[str] = None,
content_type: Optional[ContentType] = None,
metadata: Optional[Dict[str, str]] = None,
ttl: Optional[timedelta] = None,
**options: Unpack[LongRequestOptions],
) -> StorageObject:
"""Create and upload an object from a local file path.

:param file_path: Local filesystem path to read
:type file_path: str | Path
:param name: Optional object name; defaults to the file name, defaults to None
:type name: str | None, optional
:param content_type: Optional MIME type to apply to the object, defaults to None
:type content_type: ContentType | None, optional
:param metadata: Optional key-value metadata, defaults to None
:type metadata: Optional[Dict[str, str]], optional
:param name: Optional object name; defaults to the file name
:type name: Optional[str]
:param content_type: Optional MIME type to apply to the object
:type content_type: Optional[ContentType]
:param metadata: Optional key-value metadata
:type metadata: Optional[Dict[str, str]]
:param ttl: Optional Time-To-Live, after which the object is automatically deleted
:type ttl: Optional[timedelta]
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
:return: Wrapper for the uploaded object
:rtype: StorageObject
Expand All @@ -359,7 +362,8 @@ def upload_from_file(

name = name or path.name
content_type = content_type or detect_content_type(str(file_path))
obj = self.create(name=name, content_type=content_type, metadata=metadata, **options)
ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None
obj = self.create(name=name, content_type=content_type, metadata=metadata, ttl_ms=ttl_ms, **options)
obj.upload_content(content)
obj.complete()
return obj
Expand Down Expand Up @@ -407,9 +411,10 @@ def upload_from_dir(
def upload_from_text(
self,
text: str,
name: str,
*,
name: str,
metadata: Optional[Dict[str, str]] = None,
ttl: Optional[timedelta] = None,
**options: Unpack[LongRequestOptions],
) -> StorageObject:
"""Create and upload an object from a text payload.
Expand All @@ -418,24 +423,28 @@ def upload_from_text(
:type text: str
:param name: Object display name
:type name: str
:param metadata: Optional key-value metadata, defaults to None
:type metadata: Optional[Dict[str, str]], optional
:param metadata: Optional key-value metadata
:type metadata: Optional[Dict[str, str]]
:param ttl: Optional Time-To-Live, after which the object is automatically deleted
:type ttl: Optional[timedelta]
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
:return: Wrapper for the uploaded object
:rtype: StorageObject
"""
obj = self.create(name=name, content_type="text", metadata=metadata, **options)
ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None
obj = self.create(name=name, content_type="text", metadata=metadata, ttl_ms=ttl_ms, **options)
obj.upload_content(text)
obj.complete()
return obj

def upload_from_bytes(
self,
data: bytes,
name: str,
*,
name: str,
content_type: ContentType,
metadata: Optional[Dict[str, str]] = None,
ttl: Optional[timedelta] = None,
**options: Unpack[LongRequestOptions],
) -> StorageObject:
"""Create and upload an object from a bytes payload.
Expand All @@ -446,13 +455,16 @@ def upload_from_bytes(
:type name: str
:param content_type: MIME type describing the payload
:type content_type: ContentType
:param metadata: Optional key-value metadata, defaults to None
:type metadata: Optional[Dict[str, str]], optional
:param metadata: Optional key-value metadata
:type metadata: Optional[Dict[str, str]]
:param ttl: Optional Time-To-Live, after which the object is automatically deleted
:type ttl: Optional[timedelta]
:param options: See :typeddict:`~runloop_api_client.sdk._types.LongRequestOptions` for available options
:return: Wrapper for the uploaded object
:rtype: StorageObject
"""
obj = self.create(name=name, content_type=content_type, metadata=metadata, **options)
ttl_ms = int(ttl.total_seconds()) * 1000 if ttl else None
obj = self.create(name=name, content_type=content_type, metadata=metadata, ttl_ms=ttl_ms, **options)
obj.upload_content(data)
obj.complete()
return obj
Expand Down
62 changes: 46 additions & 16 deletions tests/sdk/test_async_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,20 @@ async def test_list(self, mock_async_client: AsyncMock, object_view: MockObjectV
name="test",
search="query",
starting_after="obj_000",
state="ready",
state="READ_ONLY",
)

assert len(objects) == 1
assert isinstance(objects[0], AsyncStorageObject)
assert objects[0].id == "obj_123"
mock_async_client.objects.list.assert_awaited_once()
mock_async_client.objects.list.assert_awaited_once_with(
content_type="text",
limit=10,
name="test",
search="query",
starting_after="obj_000",
state="READ_ONLY",
)

@pytest.mark.asyncio
async def test_upload_from_file(
Expand All @@ -268,9 +275,14 @@ async def test_upload_from_file(

assert isinstance(obj, AsyncStorageObject)
assert obj.id == "obj_123"
mock_async_client.objects.create.assert_awaited_once()
mock_async_client.objects.complete.assert_awaited_once()
mock_async_client.objects.create.assert_awaited_once_with(
name="test.txt",
content_type="text",
metadata=None,
ttl_ms=None,
)
http_client.put.assert_awaited_once_with(object_view.upload_url, content=b"test content")
mock_async_client.objects.complete.assert_awaited_once()

@pytest.mark.asyncio
async def test_upload_from_text(self, mock_async_client: AsyncMock, object_view: MockObjectView) -> None:
Expand All @@ -284,14 +296,15 @@ async def test_upload_from_text(self, mock_async_client: AsyncMock, object_view:
mock_async_client._client = http_client

client = AsyncStorageObjectOps(mock_async_client)
obj = await client.upload_from_text("test content", "test.txt", metadata={"key": "value"})
obj = await client.upload_from_text("test content", name="test.txt", metadata={"key": "value"})

assert isinstance(obj, AsyncStorageObject)
assert obj.id == "obj_123"
mock_async_client.objects.create.assert_awaited_once_with(
name="test.txt",
content_type="text",
metadata={"key": "value"},
ttl_ms=None,
)
http_client.put.assert_awaited_once_with(object_view.upload_url, content="test content")
mock_async_client.objects.complete.assert_awaited_once()
Expand All @@ -308,14 +321,15 @@ async def test_upload_from_bytes(self, mock_async_client: AsyncMock, object_view
mock_async_client._client = http_client

client = AsyncStorageObjectOps(mock_async_client)
obj = await client.upload_from_bytes(b"test content", "test.bin", content_type="binary")
obj = await client.upload_from_bytes(b"test content", name="test.bin", content_type="binary")

assert isinstance(obj, AsyncStorageObject)
assert obj.id == "obj_123"
mock_async_client.objects.create.assert_awaited_once_with(
name="test.bin",
content_type="binary",
metadata=None,
ttl_ms=None,
)
http_client.put.assert_awaited_once_with(object_view.upload_url, content=b"test content")
mock_async_client.objects.complete.assert_awaited_once()
Expand Down Expand Up @@ -401,10 +415,12 @@ async def test_upload_from_dir_default_name(

assert isinstance(obj, AsyncStorageObject)
# Name should be directory name + .tar.gz
mock_async_client.objects.create.assert_awaited_once()
call_args = mock_async_client.objects.create.call_args
assert call_args[1]["name"] == "my_folder.tar.gz"
assert call_args[1]["content_type"] == "tgz"
mock_async_client.objects.create.assert_awaited_once_with(
name="my_folder.tar.gz",
content_type="tgz",
metadata=None,
ttl_ms=None,
)

@pytest.mark.asyncio
async def test_upload_from_dir_with_ttl(
Expand All @@ -429,10 +445,12 @@ async def test_upload_from_dir_with_ttl(
obj = await client.upload_from_dir(test_dir, ttl=timedelta(hours=2))

assert isinstance(obj, AsyncStorageObject)
mock_async_client.objects.create.assert_awaited_once()
call_args = mock_async_client.objects.create.call_args
# 2 hours = 7200 seconds = 7200000 milliseconds
assert call_args[1]["ttl_ms"] == 7200000
mock_async_client.objects.create.assert_awaited_once_with(
name="temp_dir.tar.gz",
content_type="tgz",
metadata=None,
ttl_ms=7200000, # 2 hours = 7200 seconds = 7200000 milliseconds
)

@pytest.mark.asyncio
async def test_upload_from_dir_empty_directory(
Expand All @@ -455,7 +473,12 @@ async def test_upload_from_dir_empty_directory(

assert isinstance(obj, AsyncStorageObject)
assert obj.id == "obj_123"
mock_async_client.objects.create.assert_awaited_once()
mock_async_client.objects.create.assert_awaited_once_with(
name="empty_dir.tar.gz",
content_type="tgz",
metadata=None,
ttl_ms=None,
)
http_client.put.assert_awaited_once()
mock_async_client.objects.complete.assert_awaited_once()

Expand All @@ -482,7 +505,14 @@ async def test_upload_from_dir_with_string_path(

assert isinstance(obj, AsyncStorageObject)
assert obj.id == "obj_123"
mock_async_client.objects.create.assert_awaited_once()
mock_async_client.objects.create.assert_awaited_once_with(
name="string_path_dir.tar.gz",
content_type="tgz",
metadata=None,
ttl_ms=None,
)
http_client.put.assert_awaited_once()
mock_async_client.objects.complete.assert_awaited_once()


class TestAsyncRunloopSDK:
Expand Down
Loading