From 5a205f5121540df32c3b7a8e60f1904ae32a8968 Mon Sep 17 00:00:00 2001 From: "sdk-github-actions[bot]" Date: Mon, 7 Sep 2026 06:28:24 +0000 Subject: [PATCH] chore(release): auto --- .sdk.json | 34 +++++++- README.md | 4 + magic_hour/README.md | 1 + magic_hour/environment.py | 2 +- magic_hour/resources/v1/README.md | 1 + magic_hour/resources/v1/account/README.md | 41 +++++++++ magic_hour/resources/v1/account/__init__.py | 4 + magic_hour/resources/v1/account/client.py | 85 +++++++++++++++++++ magic_hour/resources/v1/client.py | 3 + magic_hour/types/models/__init__.py | 12 +++ .../types/models/v1_account_list_response.py | 51 +++++++++++ .../v1_account_list_response_subscription.py | 69 +++++++++++++++ ...unt_list_response_subscription_discount.py | 26 ++++++ ...ccount_list_response_subscription_price.py | 25 ++++++ pyproject.toml | 2 +- tests/test_v1_account_client.py | 65 ++++++++++++++ 16 files changed, 422 insertions(+), 3 deletions(-) create mode 100644 magic_hour/resources/v1/account/README.md create mode 100644 magic_hour/resources/v1/account/__init__.py create mode 100644 magic_hour/resources/v1/account/client.py create mode 100644 magic_hour/types/models/v1_account_list_response.py create mode 100644 magic_hour/types/models/v1_account_list_response_subscription.py create mode 100644 magic_hour/types/models/v1_account_list_response_subscription_discount.py create mode 100644 magic_hour/types/models/v1_account_list_response_subscription_price.py create mode 100644 tests/test_v1_account_client.py diff --git a/.sdk.json b/.sdk.json index 06035a1..19a73e9 100644 --- a/.sdk.json +++ b/.sdk.json @@ -1,5 +1,5 @@ { - "id": "3d6cb3a1-0777-4d71-9f1d-ee4c757afc03", + "id": "8535dd0e-5fe8-4908-8220-9772d176fe80", "tracked_paths": [ { "editable": true, @@ -29,6 +29,18 @@ "editable": true, "path": "magic_hour/resources/v1/__init__.py" }, + { + "editable": true, + "path": "magic_hour/resources/v1/account/README.md" + }, + { + "editable": true, + "path": "magic_hour/resources/v1/account/__init__.py" + }, + { + "editable": true, + "path": "magic_hour/resources/v1/account/client.py" + }, { "editable": true, "path": "magic_hour/resources/v1/ai_clothes_changer/README.md" @@ -433,6 +445,22 @@ "editable": true, "path": "magic_hour/types/models/__init__.py" }, + { + "editable": false, + "path": "magic_hour/types/models/v1_account_list_response.py" + }, + { + "editable": false, + "path": "magic_hour/types/models/v1_account_list_response_subscription.py" + }, + { + "editable": false, + "path": "magic_hour/types/models/v1_account_list_response_subscription_discount.py" + }, + { + "editable": false, + "path": "magic_hour/types/models/v1_account_list_response_subscription_price.py" + }, { "editable": false, "path": "magic_hour/types/models/v1_ai_clothes_changer_create_response.py" @@ -917,6 +945,10 @@ "editable": true, "path": "pyproject.toml" }, + { + "editable": false, + "path": "tests/test_v1_account_client.py" + }, { "editable": false, "path": "tests/test_v1_ai_clothes_changer_client.py" diff --git a/README.md b/README.md index 000dcf5..9886aac 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,10 @@ download_urls = result.downloads ## Module Documentation and Snippets +### [v1.account](magic_hour/resources/v1/account/README.md) + +- [list](magic_hour/resources/v1/account/README.md#list) - Get account details + ### [v1.ai_clothes_changer](magic_hour/resources/v1/ai_clothes_changer/README.md) - [create](magic_hour/resources/v1/ai_clothes_changer/README.md#create) - AI Clothes Changer diff --git a/magic_hour/README.md b/magic_hour/README.md index 6900d0f..27941d8 100644 --- a/magic_hour/README.md +++ b/magic_hour/README.md @@ -2,6 +2,7 @@ ## Submodules +- [account](resources/v1/account/README.md) - account - [ai_clothes_changer](resources/v1/ai_clothes_changer/README.md) - ai_clothes_changer - [ai_face_editor](resources/v1/ai_face_editor/README.md) - ai_face_editor - [ai_gif_generator](resources/v1/ai_gif_generator/README.md) - ai_gif_generator diff --git a/magic_hour/environment.py b/magic_hour/environment.py index 6d788b6..8891e75 100644 --- a/magic_hour/environment.py +++ b/magic_hour/environment.py @@ -6,7 +6,7 @@ class Environment(enum.Enum): """Pre-defined base URLs for the API""" ENVIRONMENT = "https://api.magichour.ai" - MOCK_SERVER = "https://api.sideko.dev/v1/mock/magichour/magic-hour/0.78.1" + MOCK_SERVER = "https://api.sideko.dev/v1/mock/magichour/magic-hour/0.79.0" def _get_base_url( diff --git a/magic_hour/resources/v1/README.md b/magic_hour/resources/v1/README.md index 6c72569..1e5e471 100644 --- a/magic_hour/resources/v1/README.md +++ b/magic_hour/resources/v1/README.md @@ -2,6 +2,7 @@ ## Submodules +- [account](account/README.md) - account - [ai_clothes_changer](ai_clothes_changer/README.md) - ai_clothes_changer - [ai_face_editor](ai_face_editor/README.md) - ai_face_editor - [ai_gif_generator](ai_gif_generator/README.md) - ai_gif_generator diff --git a/magic_hour/resources/v1/account/README.md b/magic_hour/resources/v1/account/README.md new file mode 100644 index 0000000..e58851a --- /dev/null +++ b/magic_hour/resources/v1/account/README.md @@ -0,0 +1,41 @@ +# v1.account + +## Module Functions + +### Get account details + +Get the current credit balance and subscription details of the account that owns the API key. + +**API Endpoint**: `GET /v1/account` + +#### Synchronous Client + +```python +from magic_hour import Client +from os import getenv + +client = Client(token=getenv("API_TOKEN")) +res = client.v1.account.list() +``` + +#### Asynchronous Client + +```python +from magic_hour import AsyncClient +from os import getenv + +client = AsyncClient(token=getenv("API_TOKEN")) +res = await client.v1.account.list() +``` + +#### Response + +##### Type + +[V1AccountListResponse](/magic_hour/types/models/v1_account_list_response.py) + +##### Example + +```python +{"credits": 12500, "email": "user@example.com", "id": "cuid-example", "subscription": {"billing_interval": "month", "cancel_at_period_end": False, "current_period_end": "2026-10-01T00:00:00.000Z", "discount": {"amount_off": 123, "percent_off": 20.0}, "name": "Pro", "price": {"amount": 4900, "currency": "usd"}, "status": "active"}, "tier": "pro"} +``` diff --git a/magic_hour/resources/v1/account/__init__.py b/magic_hour/resources/v1/account/__init__.py new file mode 100644 index 0000000..7107ec2 --- /dev/null +++ b/magic_hour/resources/v1/account/__init__.py @@ -0,0 +1,4 @@ +from .client import AccountClient, AsyncAccountClient + + +__all__ = ["AccountClient", "AsyncAccountClient"] diff --git a/magic_hour/resources/v1/account/client.py b/magic_hour/resources/v1/account/client.py new file mode 100644 index 0000000..9546fc2 --- /dev/null +++ b/magic_hour/resources/v1/account/client.py @@ -0,0 +1,85 @@ +import typing + +from magic_hour.types import models +from make_api_request import ( + AsyncBaseClient, + RequestOptions, + SyncBaseClient, + default_request_options, +) + + +class AccountClient: + def __init__(self, *, base_client: SyncBaseClient): + self._base_client = base_client + + def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> models.V1AccountListResponse: + """ + Get account details + + Get the current credit balance and subscription details of the account that owns the API key. + + GET /v1/account + + Args: + request_options: Additional options to customize the HTTP request + + Returns: + 200 + + Raises: + ApiError: A custom exception class that provides additional context + for API errors, including the HTTP status code and response body. + + Examples: + ```py + client.v1.account.list() + ``` + """ + return self._base_client.request( + method="GET", + path="/v1/account", + auth_names=["bearerAuth"], + cast_to=models.V1AccountListResponse, + request_options=request_options or default_request_options(), + ) + + +class AsyncAccountClient: + def __init__(self, *, base_client: AsyncBaseClient): + self._base_client = base_client + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> models.V1AccountListResponse: + """ + Get account details + + Get the current credit balance and subscription details of the account that owns the API key. + + GET /v1/account + + Args: + request_options: Additional options to customize the HTTP request + + Returns: + 200 + + Raises: + ApiError: A custom exception class that provides additional context + for API errors, including the HTTP status code and response body. + + Examples: + ```py + await client.v1.account.list() + ``` + """ + return await self._base_client.request( + method="GET", + path="/v1/account", + auth_names=["bearerAuth"], + cast_to=models.V1AccountListResponse, + request_options=request_options or default_request_options(), + ) diff --git a/magic_hour/resources/v1/client.py b/magic_hour/resources/v1/client.py index 125ff7f..4e06147 100644 --- a/magic_hour/resources/v1/client.py +++ b/magic_hour/resources/v1/client.py @@ -1,3 +1,4 @@ +from magic_hour.resources.v1.account import AccountClient, AsyncAccountClient from magic_hour.resources.v1.ai_clothes_changer import ( AiClothesChangerClient, AsyncAiClothesChangerClient, @@ -155,6 +156,7 @@ def __init__(self, *, base_client: SyncBaseClient): self.audio_to_video = AudioToVideoClient(base_client=self._base_client) self.ai_video_editor = AiVideoEditorClient(base_client=self._base_client) self.character_replace = CharacterReplaceClient(base_client=self._base_client) + self.account = AccountClient(base_client=self._base_client) class AsyncV1Client: @@ -213,3 +215,4 @@ def __init__(self, *, base_client: AsyncBaseClient): self.character_replace = AsyncCharacterReplaceClient( base_client=self._base_client ) + self.account = AsyncAccountClient(base_client=self._base_client) diff --git a/magic_hour/types/models/__init__.py b/magic_hour/types/models/__init__.py index 80b84b5..990fa72 100644 --- a/magic_hour/types/models/__init__.py +++ b/magic_hour/types/models/__init__.py @@ -1,3 +1,11 @@ +from .v1_account_list_response import V1AccountListResponse +from .v1_account_list_response_subscription import V1AccountListResponseSubscription +from .v1_account_list_response_subscription_discount import ( + V1AccountListResponseSubscriptionDiscount, +) +from .v1_account_list_response_subscription_price import ( + V1AccountListResponseSubscriptionPrice, +) from .v1_ai_clothes_changer_create_response import V1AiClothesChangerCreateResponse from .v1_ai_face_editor_create_response import V1AiFaceEditorCreateResponse from .v1_ai_gif_generator_create_response import V1AiGifGeneratorCreateResponse @@ -59,6 +67,10 @@ __all__ = [ + "V1AccountListResponse", + "V1AccountListResponseSubscription", + "V1AccountListResponseSubscriptionDiscount", + "V1AccountListResponseSubscriptionPrice", "V1AiClothesChangerCreateResponse", "V1AiFaceEditorCreateResponse", "V1AiGifGeneratorCreateResponse", diff --git a/magic_hour/types/models/v1_account_list_response.py b/magic_hour/types/models/v1_account_list_response.py new file mode 100644 index 0000000..1e4aafd --- /dev/null +++ b/magic_hour/types/models/v1_account_list_response.py @@ -0,0 +1,51 @@ +import pydantic +import typing +import typing_extensions + +from .v1_account_list_response_subscription import V1AccountListResponseSubscription + + +class V1AccountListResponse(pydantic.BaseModel): + """ + V1AccountListResponse + """ + + model_config = pydantic.ConfigDict( + arbitrary_types_allowed=True, + populate_by_name=True, + ) + + credits: int = pydantic.Field( + alias="credits", + ) + """ + Credits currently available to spend. Includes subscription credits and any purchased credit packs. + """ + email: typing.Optional[str] = pydantic.Field( + alias="email", + ) + """ + Email address of the account. + """ + id: str = pydantic.Field( + alias="id", + ) + """ + Unique ID of the account that owns the API key. + """ + subscription: typing.Optional[V1AccountListResponseSubscription] = pydantic.Field( + alias="subscription", + ) + """ + Details of the account's subscription plan. `null` if the account has no subscription, e.g. a free account, an account that only purchased credit packs, or an account on usage-based API pricing. + + Reflects the plan currently configured on the subscription. If a plan change is scheduled, `tier` stays on the current plan until the next payment succeeds, so `tier` and `name` can briefly disagree. + """ + tier: typing_extensions.Literal["business", "creator", "free", "pro"] = ( + pydantic.Field( + alias="tier", + ) + ) + """ + Subscription tier in effect for the account. `free` if there is no active subscription, including while a subscription is `past_due`. + """ diff --git a/magic_hour/types/models/v1_account_list_response_subscription.py b/magic_hour/types/models/v1_account_list_response_subscription.py new file mode 100644 index 0000000..922755f --- /dev/null +++ b/magic_hour/types/models/v1_account_list_response_subscription.py @@ -0,0 +1,69 @@ +import pydantic +import typing +import typing_extensions + +from .v1_account_list_response_subscription_discount import ( + V1AccountListResponseSubscriptionDiscount, +) +from .v1_account_list_response_subscription_price import ( + V1AccountListResponseSubscriptionPrice, +) + + +class V1AccountListResponseSubscription(pydantic.BaseModel): + """ + Details of the account's subscription plan. `null` if the account has no subscription, e.g. a free account, an account that only purchased credit packs, or an account on usage-based API pricing. + + Reflects the plan currently configured on the subscription. If a plan change is scheduled, `tier` stays on the current plan until the next payment succeeds, so `tier` and `name` can briefly disagree. + """ + + model_config = pydantic.ConfigDict( + arbitrary_types_allowed=True, + populate_by_name=True, + ) + + billing_interval: typing.Optional[typing_extensions.Literal["month", "year"]] = ( + pydantic.Field( + alias="billing_interval", + ) + ) + """ + How often the subscription is billed. `null` if unknown. + """ + cancel_at_period_end: bool = pydantic.Field( + alias="cancel_at_period_end", + ) + """ + Whether the subscription is scheduled to end at `current_period_end` instead of renewing. The subscription stays usable until then. + """ + current_period_end: typing.Optional[str] = pydantic.Field( + alias="current_period_end", + ) + """ + End of the current billing period, in ISO 8601 format. The subscription renews at this time, or ends if `cancel_at_period_end` is `true`. + """ + discount: typing.Optional[V1AccountListResponseSubscriptionDiscount] = ( + pydantic.Field( + alias="discount", + ) + ) + """ + Discount applied to the subscription. `null` if no discount is applied. + """ + name: typing.Optional[str] = pydantic.Field( + alias="name", + ) + """ + Name of the current subscription plan, e.g. `Creator`, `Pro`, `Pro Plus`, `Business`. `null` if the plan cannot be determined. Use `tier` for a machine-readable value. + """ + price: V1AccountListResponseSubscriptionPrice = pydantic.Field( + alias="price", + ) + status: typing_extensions.Literal["active", "past_due"] = pydantic.Field( + alias="status", + ) + """ + Status of the subscription. + - `active`: payments are up to date. + - `past_due`: the latest payment failed. `tier` is `free` until payment succeeds. The subscription is canceled if payment keeps failing. + """ diff --git a/magic_hour/types/models/v1_account_list_response_subscription_discount.py b/magic_hour/types/models/v1_account_list_response_subscription_discount.py new file mode 100644 index 0000000..4f3b144 --- /dev/null +++ b/magic_hour/types/models/v1_account_list_response_subscription_discount.py @@ -0,0 +1,26 @@ +import pydantic +import typing + + +class V1AccountListResponseSubscriptionDiscount(pydantic.BaseModel): + """ + Discount applied to the subscription. `null` if no discount is applied. + """ + + model_config = pydantic.ConfigDict( + arbitrary_types_allowed=True, + populate_by_name=True, + ) + + amount_off: typing.Optional[int] = pydantic.Field( + alias="amount_off", + ) + """ + Fixed amount taken off `price.amount` each billing interval, in the smallest unit of the currency. `null` if the discount is a percentage. + """ + percent_off: typing.Optional[float] = pydantic.Field( + alias="percent_off", + ) + """ + Percentage taken off `price.amount` each billing interval. `null` if the discount is a fixed amount. + """ diff --git a/magic_hour/types/models/v1_account_list_response_subscription_price.py b/magic_hour/types/models/v1_account_list_response_subscription_price.py new file mode 100644 index 0000000..45b7139 --- /dev/null +++ b/magic_hour/types/models/v1_account_list_response_subscription_price.py @@ -0,0 +1,25 @@ +import pydantic + + +class V1AccountListResponseSubscriptionPrice(pydantic.BaseModel): + """ + V1AccountListResponseSubscriptionPrice + """ + + model_config = pydantic.ConfigDict( + arbitrary_types_allowed=True, + populate_by_name=True, + ) + + amount: int = pydantic.Field( + alias="amount", + ) + """ + Price charged per billing interval, in the smallest unit of the currency (e.g. 4900 is $49.00 for `usd`). Discounts are not applied. + """ + currency: str = pydantic.Field( + alias="currency", + ) + """ + Three-letter ISO 4217 currency code, lowercase. + """ diff --git a/pyproject.toml b/pyproject.toml index 2e59375..5bb258b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "magic_hour" -version = "0.78.1" +version = "0.79.0" description = "Python SDK for Magic Hour API" readme = "README.md" authors = [] diff --git a/tests/test_v1_account_client.py b/tests/test_v1_account_client.py new file mode 100644 index 0000000..e2747da --- /dev/null +++ b/tests/test_v1_account_client.py @@ -0,0 +1,65 @@ +import pydantic +import pytest + +from magic_hour import AsyncClient, Client +from magic_hour.environment import Environment +from magic_hour.types import models + + +def test_list_200_success_all_params() -> None: + """Tests a GET request to the /v1/account endpoint. + + Operation: list + Test Case ID: success_all_params + Expected Status: 200 + Mode: Synchronous execution + + Response : models.V1AccountListResponse + + Validates: + - Authentication requirements are satisfied + - All required input parameters are properly handled + - Response status code is correct + - Response data matches expected schema + + This test uses example data to verify the endpoint behavior. + """ + # tests calling sync method with example data + client = Client(token="API_TOKEN", environment=Environment.MOCK_SERVER) + response = client.v1.account.list() + try: + pydantic.TypeAdapter(models.V1AccountListResponse).validate_python(response) + is_valid_response_schema = True + except pydantic.ValidationError: + is_valid_response_schema = False + assert is_valid_response_schema, "failed response type check" + + +@pytest.mark.asyncio +async def test_await_list_200_success_all_params() -> None: + """Tests a GET request to the /v1/account endpoint. + + Operation: list + Test Case ID: success_all_params + Expected Status: 200 + Mode: Asynchronous execution + + Response : models.V1AccountListResponse + + Validates: + - Authentication requirements are satisfied + - All required input parameters are properly handled + - Response status code is correct + - Response data matches expected schema + + This test uses example data to verify the endpoint behavior. + """ + # tests calling async method with example data + client = AsyncClient(token="API_TOKEN", environment=Environment.MOCK_SERVER) + response = await client.v1.account.list() + try: + pydantic.TypeAdapter(models.V1AccountListResponse).validate_python(response) + is_valid_response_schema = True + except pydantic.ValidationError: + is_valid_response_schema = False + assert is_valid_response_schema, "failed response type check"