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
5 changes: 3 additions & 2 deletions magic_hour/resources/v1/image_projects/client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import os
import time
import typing
Expand All @@ -23,7 +24,7 @@ class V1ImageProjectsGetResponseWithDownloads(models.V1ImageProjectsGetResponse)
"""
The paths to the downloaded files.

This field is only populated if `download_outputs` is True.
This field is only populated if `download_outputs` is True and the image project is complete.
"""


Expand Down Expand Up @@ -210,7 +211,7 @@ async def check_result(
while status not in ["complete", "error", "canceled"]:
api_response = await self.get(id=id)
status = api_response.status
time.sleep(poll_interval)
await asyncio.sleep(poll_interval)

if api_response.status != "complete":
log = logger.error if api_response.status == "error" else logger.info
Expand Down
19 changes: 12 additions & 7 deletions magic_hour/resources/v1/image_projects/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,12 @@ async def test_async_check_result_wait_until_complete(
DummyResponse(status="complete"),
]

monkeypatch.setattr("time.sleep", lambda _: None) # type: ignore
sleep_calls: List[float] = []

async def async_mock_sleep(seconds: float) -> None:
sleep_calls.append(seconds)

monkeypatch.setattr("asyncio.sleep", async_mock_sleep)

resp = await client.check_result(
id="xyz", wait_for_completion=True, download_outputs=False
Expand Down Expand Up @@ -439,10 +444,10 @@ async def test_async_check_result_poll_interval_default(
# Mock time.sleep to track calls
sleep_calls: List[float] = []

def mock_sleep(seconds: float) -> None:
async def async_mock_sleep(seconds: float) -> None:
sleep_calls.append(seconds)

monkeypatch.setattr("time.sleep", mock_sleep)
monkeypatch.setattr("asyncio.sleep", async_mock_sleep)

resp = await client.check_result(
id="xyz", wait_for_completion=True, download_outputs=False
Expand Down Expand Up @@ -472,10 +477,10 @@ async def test_async_check_result_poll_interval_custom(
# Mock time.sleep to track calls
sleep_calls: List[float] = []

def mock_sleep(seconds: float) -> None:
async def async_mock_sleep(seconds: float) -> None:
sleep_calls.append(seconds)

monkeypatch.setattr("time.sleep", mock_sleep)
monkeypatch.setattr("asyncio.sleep", async_mock_sleep)

resp = await client.check_result(
id="xyz", wait_for_completion=True, download_outputs=False
Expand Down Expand Up @@ -507,10 +512,10 @@ async def test_async_check_result_poll_interval_multiple_polls(
# Mock time.sleep to track calls
sleep_calls: List[float] = []

def mock_sleep(seconds: float) -> None:
async def async_mock_sleep(seconds: float) -> None:
sleep_calls.append(seconds)

monkeypatch.setattr("time.sleep", mock_sleep)
monkeypatch.setattr("asyncio.sleep", async_mock_sleep)

resp = await client.check_result(
id="xyz", wait_for_completion=True, download_outputs=False
Expand Down
136 changes: 136 additions & 0 deletions magic_hour/resources/v1/video_projects/client.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,95 @@
import os
import time
import typing
import pydantic
import logging

from magic_hour.core import (
AsyncBaseClient,
RequestOptions,
SyncBaseClient,
default_request_options,
)
from magic_hour.helpers.download import download_files_async, download_files_sync
from magic_hour.types import models

logger = logging.getLogger(__name__)


class V1VideoProjectsGetResponseWithDownloads(models.V1VideoProjectsGetResponse):
downloaded_paths: typing.Optional[typing.List[str]] = pydantic.Field(
default=None, alias="downloaded_paths"
)
"""
The paths to the downloaded files.

This field is only populated if `download_outputs` is True and the video project is complete.
"""


class VideoProjectsClient:
def __init__(self, *, base_client: SyncBaseClient):
self._base_client = base_client

def check_result(
self,
id: str,
wait_for_completion: bool,
download_outputs: bool,
download_directory: typing.Optional[str] = None,
) -> V1VideoProjectsGetResponseWithDownloads:
"""
Check the result of a video project with optional waiting and downloading.

This method retrieves the status of a video project and optionally waits for completion
and downloads the output files.

Args:
id: Unique ID of the video project
wait_for_completion: Whether to wait for the video project to complete
download_outputs: Whether to download the outputs
download_directory: The directory to download the outputs to. If not provided,
the outputs will be downloaded to the current working directory

Returns:
V1VideoProjectsGetResponseWithDownloads: The video project response with optional
downloaded file paths included
"""
api_response = self.get(id=id)
if not wait_for_completion:
response = V1VideoProjectsGetResponseWithDownloads(
**api_response.model_dump()
)
return response

poll_interval = float(os.getenv("MAGIC_HOUR_POLL_INTERVAL", "0.5"))

status = api_response.status

while status not in ["complete", "error", "canceled"]:
api_response = self.get(id=id)
status = api_response.status
time.sleep(poll_interval)

if api_response.status != "complete":
log = logger.error if api_response.status == "error" else logger.info
log(
f"Video project {id} has status {api_response.status}: {api_response.error}"
)
return V1VideoProjectsGetResponseWithDownloads(**api_response.model_dump())

if not download_outputs:
return V1VideoProjectsGetResponseWithDownloads(**api_response.model_dump())

downloaded_paths = download_files_sync(
downloads=api_response.downloads,
download_directory=download_directory,
)

return V1VideoProjectsGetResponseWithDownloads(
**api_response.model_dump(), downloaded_paths=downloaded_paths
)

def delete(
self, *, id: str, request_options: typing.Optional[RequestOptions] = None
) -> None:
Expand Down Expand Up @@ -95,6 +172,65 @@ class AsyncVideoProjectsClient:
def __init__(self, *, base_client: AsyncBaseClient):
self._base_client = base_client

async def check_result(
self,
id: str,
wait_for_completion: bool,
download_outputs: bool,
download_directory: typing.Optional[str] = None,
) -> V1VideoProjectsGetResponseWithDownloads:
"""
Check the result of a video project with optional waiting and downloading.

This method retrieves the status of a video project and optionally waits for completion
and downloads the output files.

Args:
id: Unique ID of the video project
wait_for_completion: Whether to wait for the video project to complete
download_outputs: Whether to download the outputs
download_directory: The directory to download the outputs to. If not provided,
the outputs will be downloaded to the current working directory

Returns:
V1VideoProjectsGetResponseWithDownloads: The video project response with optional
downloaded file paths included
"""
api_response = await self.get(id=id)
if not wait_for_completion:
response = V1VideoProjectsGetResponseWithDownloads(
**api_response.model_dump()
)
return response

poll_interval = float(os.getenv("MAGIC_HOUR_POLL_INTERVAL", "0.5"))

status = api_response.status

while status not in ["complete", "error", "canceled"]:
api_response = await self.get(id=id)
status = api_response.status
time.sleep(poll_interval)

if api_response.status != "complete":
log = logger.error if api_response.status == "error" else logger.info
log(
f"Video project {id} has status {api_response.status}: {api_response.error}"
)
return V1VideoProjectsGetResponseWithDownloads(**api_response.model_dump())

if not download_outputs:
return V1VideoProjectsGetResponseWithDownloads(**api_response.model_dump())

downloaded_paths = await download_files_async(
downloads=api_response.downloads,
download_directory=download_directory,
)

return V1VideoProjectsGetResponseWithDownloads(
**api_response.model_dump(), downloaded_paths=downloaded_paths
)

async def delete(
self, *, id: str, request_options: typing.Optional[RequestOptions] = None
) -> None:
Expand Down
Loading