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
31 changes: 31 additions & 0 deletions magic_hour/resources/v1/files/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
import typing
import io
import pathlib
import logging

logger = logging.getLogger(__name__)


def _get_file_type_and_extension(file_path: str):
Expand Down Expand Up @@ -162,6 +165,8 @@ def upload_file(
Args:
file: The file to upload. Can be:
- **str**: Path to a local file (e.g., "/path/to/image.jpg")
- **str**: URL of the file to upload, this will be skipped and the URL will be returned as is
- **str**: if the string begins with "api-assets", the file will be assumed to be a blob path and already uploaded to Magic Hour's storage
- **pathlib.Path**: Path object to a local file
- **typing.BinaryIO or io.IOBase**: File-like object (must have a 'name' attribute)

Expand Down Expand Up @@ -210,6 +215,16 @@ def upload_file(
file_path = client.v1.files.upload_file(video_file)
```
"""

if isinstance(file, str) and file.startswith(("http://", "https://")):
logger.info(f"{file} is a url. Skipping upload and returning the URL.")
return file
elif isinstance(file, str) and file.startswith("api-assets"):
logger.info(
f"{file} is begins with api-assets, assuming it's a blob path.. Skipping upload and returning the path."
)
return file

file_path, file_to_upload, file_type, extension = _process_file_input(file)

response = self.upload_urls.create(
Expand All @@ -233,6 +248,9 @@ def upload_file(
upload_response = client.put(url=upload_info.upload_url, content=content)
upload_response.raise_for_status()

logger.info(
f"Uploaded {file_path} to Magic Hour storage at {upload_info.file_path}."
)
return upload_info.file_path


Expand Down Expand Up @@ -269,6 +287,8 @@ async def upload_file(
Args:
file: The file to upload. Can be:
- **str**: Path to a local file (e.g., "/path/to/image.jpg")
- **str**: URL of the file to upload, this will be skipped and the URL will be returned as is
- **str**: if the string begins with "api-assets", the file will be assumed to be a blob path and already uploaded to Magic Hour's storage
- **pathlib.Path**: Path object to a local file
- **typing.BinaryIO or io.IOBase**: File-like object (must have a 'name' attribute)

Expand Down Expand Up @@ -305,6 +325,14 @@ async def upload_example():
asyncio.run(upload_example())
```
"""
if isinstance(file, str) and file.startswith(("http://", "https://")):
logger.info(f"{file} is a url. Skipping upload and returning the URL.")
return file
elif isinstance(file, str) and file.startswith("api-assets"):
logger.info(
f"{file} is begins with api-assets, assuming it's a blob path.. Skipping upload and returning the path."
)
return file

file_path, file_to_upload, file_type, extension = _process_file_input(file)

Expand All @@ -331,4 +359,7 @@ async def upload_example():
)
upload_response.raise_for_status()

logger.info(
f"Uploaded {file_path} to Magic Hour storage at {upload_info.file_path}."
)
return upload_info.file_path
141 changes: 141 additions & 0 deletions magic_hour/resources/v1/files/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,147 @@ def test_upload_different_file_types():
os.remove(tmp_path)


# Test URL handling - should skip upload and return URL as is
def test_upload_file_with_http_url():
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)

http_url = "http://example.com/image.jpg"
result = client.v1.files.upload_file(http_url)

assert result == http_url


def test_upload_file_with_https_url():
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)

https_url = "https://example.com/video.mp4"
result = client.v1.files.upload_file(https_url)

assert result == https_url


@pytest.mark.asyncio
async def test_async_upload_file_with_http_url():
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)

http_url = "http://example.com/audio.mp3"
result = await client.v1.files.upload_file(http_url)

assert result == http_url


@pytest.mark.asyncio
async def test_async_upload_file_with_https_url():
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)

https_url = "https://example.com/document.pdf"
result = await client.v1.files.upload_file(https_url)

assert result == https_url


# Test blob path handling - should skip upload and return blob path as is
def test_upload_file_with_blob_path():
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)

blob_path = "api-assets/user123/image.jpg"
result = client.v1.files.upload_file(blob_path)

assert result == blob_path


def test_upload_file_with_blob_path_different_format():
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)

blob_path = "api-assets/project456/video.mp4"
result = client.v1.files.upload_file(blob_path)

assert result == blob_path


@pytest.mark.asyncio
async def test_async_upload_file_with_blob_path():
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)

blob_path = "api-assets/user789/audio.wav"
result = await client.v1.files.upload_file(blob_path)

assert result == blob_path


@pytest.mark.asyncio
async def test_async_upload_file_with_blob_path_different_format():
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)

blob_path = "api-assets/session101/photo.png"
result = await client.v1.files.upload_file(blob_path)

assert result == blob_path


# Test that URL and blob path handling doesn't make HTTP requests
def test_upload_file_with_url_does_not_make_http_requests():
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)

with mock.patch("httpx.Client.put") as mock_put:
with mock.patch.object(client.v1.files.upload_urls, "create") as mock_create:
result = client.v1.files.upload_file("https://example.com/file.jpg")

# Should not call upload_urls.create or make HTTP PUT request
mock_create.assert_not_called()
mock_put.assert_not_called()

assert result == "https://example.com/file.jpg"


def test_upload_file_with_blob_path_does_not_make_http_requests():
client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER)

with mock.patch("httpx.Client.put") as mock_put:
with mock.patch.object(client.v1.files.upload_urls, "create") as mock_create:
result = client.v1.files.upload_file("api-assets/user123/file.mp4")

# Should not call upload_urls.create or make HTTP PUT request
mock_create.assert_not_called()
mock_put.assert_not_called()

assert result == "api-assets/user123/file.mp4"


@pytest.mark.asyncio
async def test_async_upload_file_with_url_does_not_make_http_requests():
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)

with mock.patch("httpx.AsyncClient.put", new_callable=mock.AsyncMock) as mock_put:
with mock.patch.object(
client.v1.files.upload_urls, "create", new_callable=mock.AsyncMock
) as mock_create:
result = await client.v1.files.upload_file("http://example.com/file.mp3")

# Should not call upload_urls.create or make HTTP PUT request
mock_create.assert_not_awaited()
mock_put.assert_not_awaited()

assert result == "http://example.com/file.mp3"


@pytest.mark.asyncio
async def test_async_upload_file_with_blob_path_does_not_make_http_requests():
client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER)

with mock.patch("httpx.AsyncClient.put", new_callable=mock.AsyncMock) as mock_put:
with mock.patch.object(
client.v1.files.upload_urls, "create", new_callable=mock.AsyncMock
) as mock_create:
result = await client.v1.files.upload_file("api-assets/user456/file.wav")

# Should not call upload_urls.create or make HTTP PUT request
mock_create.assert_not_awaited()
mock_put.assert_not_awaited()

assert result == "api-assets/user456/file.wav"


# Test file position preservation for file-like objects
def test_upload_file_preserves_file_position():
data = b"test data for position preservation"
Expand Down