diff --git a/reference.md b/reference.md index ca03cf1f..54899f1e 100644 --- a/reference.md +++ b/reference.md @@ -2912,6 +2912,93 @@ client.manage.v1.projects.billing.breakdown.list( + + + + +## Manage V1 Projects Billing Fields +
client.manage.v1.projects.billing.fields.list(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Lists the accessors, deployment types, tags, and line items used for billing data in the specified time period. Use this endpoint if you want to filter your results from the Billing Breakdown endpoint and want to know what filters are available. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from deepgram import DeepgramClient + +client = DeepgramClient( + api_key="YOUR_API_KEY", +) +client.manage.v1.projects.billing.fields.list( + project_id="123456-7890-1234-5678-901234", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**project_id:** `str` — The unique identifier of the project + +
+
+ +
+
+ +**start:** `typing.Optional[str]` — Start date of the requested date range. Format accepted is YYYY-MM-DD + +
+
+ +
+
+ +**end:** `typing.Optional[str]` — End date of the requested date range. Format accepted is YYYY-MM-DD + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
diff --git a/src/deepgram/__init__.py b/src/deepgram/__init__.py index 5f92b5ea..e4d9cf16 100644 --- a/src/deepgram/__init__.py +++ b/src/deepgram/__init__.py @@ -51,6 +51,8 @@ GetProjectV1Response, GrantV1Response, LeaveProjectV1Response, + ListBillingFieldsV1Response, + ListBillingFieldsV1ResponseDeploymentsItem, ListModelsV1Response, ListModelsV1ResponseSttModels, ListModelsV1ResponseTtsModels, @@ -223,6 +225,7 @@ GetProjectV1ResponseParams, GrantV1ResponseParams, LeaveProjectV1ResponseParams, + ListBillingFieldsV1ResponseParams, ListModelsV1ResponseParams, ListModelsV1ResponseSttModelsParams, ListModelsV1ResponseTtsModelsMetadataParams, @@ -399,6 +402,9 @@ "GrantV1ResponseParams": ".requests", "LeaveProjectV1Response": ".types", "LeaveProjectV1ResponseParams": ".requests", + "ListBillingFieldsV1Response": ".types", + "ListBillingFieldsV1ResponseDeploymentsItem": ".types", + "ListBillingFieldsV1ResponseParams": ".requests", "ListModelsV1Response": ".types", "ListModelsV1ResponseParams": ".requests", "ListModelsV1ResponseSttModels": ".types", @@ -732,6 +738,9 @@ def __dir__(): "GrantV1ResponseParams", "LeaveProjectV1Response", "LeaveProjectV1ResponseParams", + "ListBillingFieldsV1Response", + "ListBillingFieldsV1ResponseDeploymentsItem", + "ListBillingFieldsV1ResponseParams", "ListModelsV1Response", "ListModelsV1ResponseParams", "ListModelsV1ResponseSttModels", diff --git a/src/deepgram/manage/v1/projects/billing/__init__.py b/src/deepgram/manage/v1/projects/billing/__init__.py index 21fbe150..dd9d6201 100644 --- a/src/deepgram/manage/v1/projects/billing/__init__.py +++ b/src/deepgram/manage/v1/projects/billing/__init__.py @@ -6,13 +6,14 @@ from importlib import import_module if typing.TYPE_CHECKING: - from . import balances, breakdown, purchases + from . import balances, breakdown, fields, purchases from .breakdown import BreakdownListRequestDeployment, BreakdownListRequestGroupingItem _dynamic_imports: typing.Dict[str, str] = { "BreakdownListRequestDeployment": ".breakdown", "BreakdownListRequestGroupingItem": ".breakdown", "balances": ".balances", "breakdown": ".breakdown", + "fields": ".fields", "purchases": ".purchases", } @@ -38,4 +39,11 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["BreakdownListRequestDeployment", "BreakdownListRequestGroupingItem", "balances", "breakdown", "purchases"] +__all__ = [ + "BreakdownListRequestDeployment", + "BreakdownListRequestGroupingItem", + "balances", + "breakdown", + "fields", + "purchases", +] diff --git a/src/deepgram/manage/v1/projects/billing/client.py b/src/deepgram/manage/v1/projects/billing/client.py index 90dc0bb6..ad8b0fef 100644 --- a/src/deepgram/manage/v1/projects/billing/client.py +++ b/src/deepgram/manage/v1/projects/billing/client.py @@ -10,6 +10,7 @@ if typing.TYPE_CHECKING: from .balances.client import AsyncBalancesClient, BalancesClient from .breakdown.client import AsyncBreakdownClient, BreakdownClient + from .fields.client import AsyncFieldsClient, FieldsClient from .purchases.client import AsyncPurchasesClient, PurchasesClient @@ -19,6 +20,7 @@ def __init__(self, *, client_wrapper: SyncClientWrapper): self._client_wrapper = client_wrapper self._balances: typing.Optional[BalancesClient] = None self._breakdown: typing.Optional[BreakdownClient] = None + self._fields: typing.Optional[FieldsClient] = None self._purchases: typing.Optional[PurchasesClient] = None @property @@ -48,6 +50,14 @@ def breakdown(self): self._breakdown = BreakdownClient(client_wrapper=self._client_wrapper) return self._breakdown + @property + def fields(self): + if self._fields is None: + from .fields.client import FieldsClient # noqa: E402 + + self._fields = FieldsClient(client_wrapper=self._client_wrapper) + return self._fields + @property def purchases(self): if self._purchases is None: @@ -63,6 +73,7 @@ def __init__(self, *, client_wrapper: AsyncClientWrapper): self._client_wrapper = client_wrapper self._balances: typing.Optional[AsyncBalancesClient] = None self._breakdown: typing.Optional[AsyncBreakdownClient] = None + self._fields: typing.Optional[AsyncFieldsClient] = None self._purchases: typing.Optional[AsyncPurchasesClient] = None @property @@ -92,6 +103,14 @@ def breakdown(self): self._breakdown = AsyncBreakdownClient(client_wrapper=self._client_wrapper) return self._breakdown + @property + def fields(self): + if self._fields is None: + from .fields.client import AsyncFieldsClient # noqa: E402 + + self._fields = AsyncFieldsClient(client_wrapper=self._client_wrapper) + return self._fields + @property def purchases(self): if self._purchases is None: diff --git a/src/deepgram/manage/v1/projects/billing/fields/__init__.py b/src/deepgram/manage/v1/projects/billing/fields/__init__.py new file mode 100644 index 00000000..5cde0202 --- /dev/null +++ b/src/deepgram/manage/v1/projects/billing/fields/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/deepgram/manage/v1/projects/billing/fields/client.py b/src/deepgram/manage/v1/projects/billing/fields/client.py new file mode 100644 index 00000000..00682a0d --- /dev/null +++ b/src/deepgram/manage/v1/projects/billing/fields/client.py @@ -0,0 +1,136 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ......core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ......core.request_options import RequestOptions +from ......types.list_billing_fields_v1response import ListBillingFieldsV1Response +from .raw_client import AsyncRawFieldsClient, RawFieldsClient + + +class FieldsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawFieldsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawFieldsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawFieldsClient + """ + return self._raw_client + + def list( + self, + project_id: str, + *, + start: typing.Optional[str] = None, + end: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListBillingFieldsV1Response: + """ + Lists the accessors, deployment types, tags, and line items used for billing data in the specified time period. Use this endpoint if you want to filter your results from the Billing Breakdown endpoint and want to know what filters are available. + + Parameters + ---------- + project_id : str + The unique identifier of the project + + start : typing.Optional[str] + Start date of the requested date range. Format accepted is YYYY-MM-DD + + end : typing.Optional[str] + End date of the requested date range. Format accepted is YYYY-MM-DD + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListBillingFieldsV1Response + A list of billing fields for a specific project + + Examples + -------- + from deepgram import DeepgramClient + + client = DeepgramClient( + api_key="YOUR_API_KEY", + ) + client.manage.v1.projects.billing.fields.list( + project_id="123456-7890-1234-5678-901234", + ) + """ + _response = self._raw_client.list(project_id, start=start, end=end, request_options=request_options) + return _response.data + + +class AsyncFieldsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawFieldsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawFieldsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawFieldsClient + """ + return self._raw_client + + async def list( + self, + project_id: str, + *, + start: typing.Optional[str] = None, + end: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ListBillingFieldsV1Response: + """ + Lists the accessors, deployment types, tags, and line items used for billing data in the specified time period. Use this endpoint if you want to filter your results from the Billing Breakdown endpoint and want to know what filters are available. + + Parameters + ---------- + project_id : str + The unique identifier of the project + + start : typing.Optional[str] + Start date of the requested date range. Format accepted is YYYY-MM-DD + + end : typing.Optional[str] + End date of the requested date range. Format accepted is YYYY-MM-DD + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListBillingFieldsV1Response + A list of billing fields for a specific project + + Examples + -------- + import asyncio + + from deepgram import AsyncDeepgramClient + + client = AsyncDeepgramClient( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.manage.v1.projects.billing.fields.list( + project_id="123456-7890-1234-5678-901234", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(project_id, start=start, end=end, request_options=request_options) + return _response.data diff --git a/src/deepgram/manage/v1/projects/billing/fields/raw_client.py b/src/deepgram/manage/v1/projects/billing/fields/raw_client.py new file mode 100644 index 00000000..6ed0b2b3 --- /dev/null +++ b/src/deepgram/manage/v1/projects/billing/fields/raw_client.py @@ -0,0 +1,155 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ......core.api_error import ApiError +from ......core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ......core.http_response import AsyncHttpResponse, HttpResponse +from ......core.jsonable_encoder import jsonable_encoder +from ......core.pydantic_utilities import parse_obj_as +from ......core.request_options import RequestOptions +from ......errors.bad_request_error import BadRequestError +from ......types.list_billing_fields_v1response import ListBillingFieldsV1Response + + +class RawFieldsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, + project_id: str, + *, + start: typing.Optional[str] = None, + end: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ListBillingFieldsV1Response]: + """ + Lists the accessors, deployment types, tags, and line items used for billing data in the specified time period. Use this endpoint if you want to filter your results from the Billing Breakdown endpoint and want to know what filters are available. + + Parameters + ---------- + project_id : str + The unique identifier of the project + + start : typing.Optional[str] + Start date of the requested date range. Format accepted is YYYY-MM-DD + + end : typing.Optional[str] + End date of the requested date range. Format accepted is YYYY-MM-DD + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListBillingFieldsV1Response] + A list of billing fields for a specific project + """ + _response = self._client_wrapper.httpx_client.request( + f"v1/projects/{jsonable_encoder(project_id)}/billing/fields", + base_url=self._client_wrapper.get_environment().base, + method="GET", + params={ + "start": start, + "end": end, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListBillingFieldsV1Response, + parse_obj_as( + type_=ListBillingFieldsV1Response, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + parse_obj_as( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawFieldsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, + project_id: str, + *, + start: typing.Optional[str] = None, + end: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ListBillingFieldsV1Response]: + """ + Lists the accessors, deployment types, tags, and line items used for billing data in the specified time period. Use this endpoint if you want to filter your results from the Billing Breakdown endpoint and want to know what filters are available. + + Parameters + ---------- + project_id : str + The unique identifier of the project + + start : typing.Optional[str] + Start date of the requested date range. Format accepted is YYYY-MM-DD + + end : typing.Optional[str] + End date of the requested date range. Format accepted is YYYY-MM-DD + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListBillingFieldsV1Response] + A list of billing fields for a specific project + """ + _response = await self._client_wrapper.httpx_client.request( + f"v1/projects/{jsonable_encoder(project_id)}/billing/fields", + base_url=self._client_wrapper.get_environment().base, + method="GET", + params={ + "start": start, + "end": end, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListBillingFieldsV1Response, + parse_obj_as( + type_=ListBillingFieldsV1Response, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Optional[typing.Any], + parse_obj_as( + type_=typing.Optional[typing.Any], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/deepgram/requests/__init__.py b/src/deepgram/requests/__init__.py index c1ee786d..42b13057 100644 --- a/src/deepgram/requests/__init__.py +++ b/src/deepgram/requests/__init__.py @@ -53,6 +53,7 @@ from .get_project_v1response import GetProjectV1ResponseParams from .grant_v1response import GrantV1ResponseParams from .leave_project_v1response import LeaveProjectV1ResponseParams + from .list_billing_fields_v1response import ListBillingFieldsV1ResponseParams from .list_models_v1response import ListModelsV1ResponseParams from .list_models_v1response_stt_models import ListModelsV1ResponseSttModelsParams from .list_models_v1response_tts_models import ListModelsV1ResponseTtsModelsParams @@ -207,6 +208,7 @@ "GetProjectV1ResponseParams": ".get_project_v1response", "GrantV1ResponseParams": ".grant_v1response", "LeaveProjectV1ResponseParams": ".leave_project_v1response", + "ListBillingFieldsV1ResponseParams": ".list_billing_fields_v1response", "ListModelsV1ResponseParams": ".list_models_v1response", "ListModelsV1ResponseSttModelsParams": ".list_models_v1response_stt_models", "ListModelsV1ResponseTtsModelsMetadataParams": ".list_models_v1response_tts_models_metadata", @@ -357,6 +359,7 @@ def __dir__(): "GetProjectV1ResponseParams", "GrantV1ResponseParams", "LeaveProjectV1ResponseParams", + "ListBillingFieldsV1ResponseParams", "ListModelsV1ResponseParams", "ListModelsV1ResponseSttModelsParams", "ListModelsV1ResponseTtsModelsMetadataParams", diff --git a/src/deepgram/requests/list_billing_fields_v1response.py b/src/deepgram/requests/list_billing_fields_v1response.py new file mode 100644 index 00000000..b4952795 --- /dev/null +++ b/src/deepgram/requests/list_billing_fields_v1response.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..types.list_billing_fields_v1response_deployments_item import ListBillingFieldsV1ResponseDeploymentsItem + + +class ListBillingFieldsV1ResponseParams(typing_extensions.TypedDict): + accessors: typing_extensions.NotRequired[typing.Sequence[str]] + """ + List of accessor UUIDs for the time period + """ + + deployments: typing_extensions.NotRequired[typing.Sequence[ListBillingFieldsV1ResponseDeploymentsItem]] + """ + List of deployment types for the time period + """ + + tags: typing_extensions.NotRequired[typing.Sequence[str]] + """ + List of tags for the time period + """ + + line_items: typing_extensions.NotRequired[typing.Dict[str, str]] + """ + Map of line item names to human-readable descriptions for the time period + """ diff --git a/src/deepgram/types/__init__.py b/src/deepgram/types/__init__.py index 5171b729..bed947cd 100644 --- a/src/deepgram/types/__init__.py +++ b/src/deepgram/types/__init__.py @@ -58,6 +58,8 @@ from .get_project_v1response import GetProjectV1Response from .grant_v1response import GrantV1Response from .leave_project_v1response import LeaveProjectV1Response + from .list_billing_fields_v1response import ListBillingFieldsV1Response + from .list_billing_fields_v1response_deployments_item import ListBillingFieldsV1ResponseDeploymentsItem from .list_models_v1response import ListModelsV1Response from .list_models_v1response_stt_models import ListModelsV1ResponseSttModels from .list_models_v1response_tts_models import ListModelsV1ResponseTtsModels @@ -254,6 +256,8 @@ "GetProjectV1Response": ".get_project_v1response", "GrantV1Response": ".grant_v1response", "LeaveProjectV1Response": ".leave_project_v1response", + "ListBillingFieldsV1Response": ".list_billing_fields_v1response", + "ListBillingFieldsV1ResponseDeploymentsItem": ".list_billing_fields_v1response_deployments_item", "ListModelsV1Response": ".list_models_v1response", "ListModelsV1ResponseSttModels": ".list_models_v1response_stt_models", "ListModelsV1ResponseTtsModels": ".list_models_v1response_tts_models", @@ -450,6 +454,8 @@ def __dir__(): "GetProjectV1Response", "GrantV1Response", "LeaveProjectV1Response", + "ListBillingFieldsV1Response", + "ListBillingFieldsV1ResponseDeploymentsItem", "ListModelsV1Response", "ListModelsV1ResponseSttModels", "ListModelsV1ResponseTtsModels", diff --git a/src/deepgram/types/list_billing_fields_v1response.py b/src/deepgram/types/list_billing_fields_v1response.py new file mode 100644 index 00000000..3e35b2a3 --- /dev/null +++ b/src/deepgram/types/list_billing_fields_v1response.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .list_billing_fields_v1response_deployments_item import ListBillingFieldsV1ResponseDeploymentsItem + + +class ListBillingFieldsV1Response(UniversalBaseModel): + accessors: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + List of accessor UUIDs for the time period + """ + + deployments: typing.Optional[typing.List[ListBillingFieldsV1ResponseDeploymentsItem]] = pydantic.Field(default=None) + """ + List of deployment types for the time period + """ + + tags: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + List of tags for the time period + """ + + line_items: typing.Optional[typing.Dict[str, str]] = pydantic.Field(default=None) + """ + Map of line item names to human-readable descriptions for the time period + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/deepgram/types/list_billing_fields_v1response_deployments_item.py b/src/deepgram/types/list_billing_fields_v1response_deployments_item.py new file mode 100644 index 00000000..bf0792c5 --- /dev/null +++ b/src/deepgram/types/list_billing_fields_v1response_deployments_item.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ListBillingFieldsV1ResponseDeploymentsItem = typing.Union[ + typing.Literal["hosted", "beta", "self-hosted", "dedicated"], typing.Any +] diff --git a/tests/unit/test_manage_billing_fields.py b/tests/unit/test_manage_billing_fields.py new file mode 100644 index 00000000..76f8aadd --- /dev/null +++ b/tests/unit/test_manage_billing_fields.py @@ -0,0 +1,498 @@ +""" +Unit tests for manage projects billing fields models and methods. + +This module tests the billing fields list methods including: +- ListBillingFieldsV1Response model validation +- Sync and async client methods +- Request parameter handling +- Error scenarios +""" + +import pytest +from pydantic import ValidationError + +from deepgram.types.list_billing_fields_v1response import ListBillingFieldsV1Response +from deepgram.types.list_billing_fields_v1response_deployments_item import ( + ListBillingFieldsV1ResponseDeploymentsItem, +) + + +class TestListBillingFieldsV1Response: + """Test ListBillingFieldsV1Response model.""" + + def test_valid_billing_fields_response_full(self): + """Test creating a valid billing fields response with all fields.""" + response_data = { + "accessors": [ + "12345678-1234-1234-1234-123456789012", + "87654321-4321-4321-4321-210987654321", + ], + "deployments": ["hosted", "beta", "self-hosted", "dedicated"], + "tags": ["tag1", "tag2", "production"], + "line_items": { + "streaming::nova-3": "Nova-3 Streaming", + "batch::nova-2": "Nova-2 Batch", + "streaming::enhanced": "Enhanced Streaming", + }, + } + + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors is not None + assert len(response.accessors) == 2 + assert response.accessors[0] == "12345678-1234-1234-1234-123456789012" + assert response.deployments is not None + assert len(response.deployments) == 4 + assert "hosted" in response.deployments + assert response.tags is not None + assert len(response.tags) == 3 + assert "production" in response.tags + assert response.line_items is not None + assert len(response.line_items) == 3 + assert response.line_items["streaming::nova-3"] == "Nova-3 Streaming" + + def test_valid_billing_fields_response_minimal(self): + """Test creating a billing fields response with minimal fields.""" + response_data = {} + + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors is None + assert response.deployments is None + assert response.tags is None + assert response.line_items is None + + def test_billing_fields_response_empty_lists(self): + """Test billing fields response with empty lists.""" + response_data = { + "accessors": [], + "deployments": [], + "tags": [], + "line_items": {}, + } + + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors == [] + assert response.deployments == [] + assert response.tags == [] + assert response.line_items == {} + + def test_billing_fields_response_with_accessors_only(self): + """Test billing fields response with only accessors.""" + response_data = { + "accessors": [ + "11111111-1111-1111-1111-111111111111", + "22222222-2222-2222-2222-222222222222", + ] + } + + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors is not None + assert len(response.accessors) == 2 + assert response.deployments is None + assert response.tags is None + assert response.line_items is None + + def test_billing_fields_response_with_deployments_only(self): + """Test billing fields response with only deployments.""" + response_data = {"deployments": ["hosted", "dedicated"]} + + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors is None + assert response.deployments is not None + assert len(response.deployments) == 2 + assert "hosted" in response.deployments + assert "dedicated" in response.deployments + assert response.tags is None + assert response.line_items is None + + def test_billing_fields_response_with_tags_only(self): + """Test billing fields response with only tags.""" + response_data = {"tags": ["development", "staging", "production"]} + + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors is None + assert response.deployments is None + assert response.tags is not None + assert len(response.tags) == 3 + assert "production" in response.tags + assert response.line_items is None + + def test_billing_fields_response_with_line_items_only(self): + """Test billing fields response with only line_items.""" + response_data = { + "line_items": { + "streaming::nova-3": "Nova-3 Streaming", + "batch::whisper": "Whisper Batch", + } + } + + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors is None + assert response.deployments is None + assert response.tags is None + assert response.line_items is not None + assert len(response.line_items) == 2 + assert response.line_items["batch::whisper"] == "Whisper Batch" + + def test_billing_fields_response_serialization(self): + """Test billing fields response serialization.""" + response_data = { + "accessors": ["12345678-1234-1234-1234-123456789012"], + "deployments": ["hosted"], + "tags": ["test-tag"], + "line_items": {"streaming::nova-3": "Nova-3 Streaming"}, + } + + response = ListBillingFieldsV1Response(**response_data) + + # Test dict conversion + response_dict = response.model_dump() + assert "accessors" in response_dict + assert "deployments" in response_dict + assert "tags" in response_dict + assert "line_items" in response_dict + assert response_dict["accessors"][0] == "12345678-1234-1234-1234-123456789012" + + # Test JSON serialization + json_str = response.model_dump_json() + assert '"accessors"' in json_str + assert '"deployments"' in json_str + assert '"tags"' in json_str + assert '"line_items"' in json_str + assert "12345678-1234-1234-1234-123456789012" in json_str + + def test_billing_fields_response_immutability(self): + """Test that billing fields response is immutable (frozen).""" + response = ListBillingFieldsV1Response( + accessors=["12345678-1234-1234-1234-123456789012"] + ) + + with pytest.raises((AttributeError, ValidationError)): + response.accessors = ["new-accessor"] + + def test_billing_fields_response_extra_fields_allowed(self): + """Test that billing fields response allows extra fields.""" + response_data = { + "accessors": ["12345678-1234-1234-1234-123456789012"], + "extra_field": "extra_value", + "custom_data": {"nested": "value"}, + } + + # Should not raise an error due to extra="allow" + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors is not None + assert hasattr(response, "extra_field") + assert hasattr(response, "custom_data") + + def test_billing_fields_response_roundtrip_serialization(self): + """Test that billing fields response can be serialized and deserialized.""" + original_data = { + "accessors": [ + "12345678-1234-1234-1234-123456789012", + "87654321-4321-4321-4321-210987654321", + ], + "deployments": ["hosted", "beta"], + "tags": ["tag1", "tag2"], + "line_items": { + "streaming::nova-3": "Nova-3 Streaming", + "batch::nova-2": "Nova-2 Batch", + }, + } + + original_response = ListBillingFieldsV1Response(**original_data) + + # Serialize to JSON and back + json_str = original_response.model_dump_json() + import json + + parsed_data = json.loads(json_str) + reconstructed_response = ListBillingFieldsV1Response(**parsed_data) + + assert original_response.accessors == reconstructed_response.accessors + assert original_response.deployments == reconstructed_response.deployments + assert original_response.tags == reconstructed_response.tags + assert original_response.line_items == reconstructed_response.line_items + + def test_billing_fields_response_with_many_accessors(self): + """Test billing fields response with many accessors.""" + # Simulate a response with many accessors + accessors = [ + f"{i:08x}-1234-1234-1234-123456789012" for i in range(100) + ] + response_data = {"accessors": accessors} + + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors is not None + assert len(response.accessors) == 100 + assert response.accessors[0] == "00000000-1234-1234-1234-123456789012" + assert response.accessors[99] == "00000063-1234-1234-1234-123456789012" + + def test_billing_fields_response_with_many_tags(self): + """Test billing fields response with many tags.""" + # Simulate a response with many tags + tags = [f"tag-{i}" for i in range(50)] + response_data = {"tags": tags} + + response = ListBillingFieldsV1Response(**response_data) + + assert response.tags is not None + assert len(response.tags) == 50 + assert "tag-0" in response.tags + assert "tag-49" in response.tags + + def test_billing_fields_response_with_many_line_items(self): + """Test billing fields response with many line_items.""" + # Simulate a response with many line items + line_items = { + f"streaming::model-{i}": f"Model {i} Streaming" for i in range(20) + } + response_data = {"line_items": line_items} + + response = ListBillingFieldsV1Response(**response_data) + + assert response.line_items is not None + assert len(response.line_items) == 20 + assert response.line_items["streaming::model-0"] == "Model 0 Streaming" + assert response.line_items["streaming::model-19"] == "Model 19 Streaming" + + def test_billing_fields_response_with_special_characters_in_tags(self): + """Test billing fields response with special characters in tags.""" + response_data = { + "tags": [ + "tag-with-dashes", + "tag_with_underscores", + "tag.with.dots", + "tag:with:colons", + "tag/with/slashes", + ] + } + + response = ListBillingFieldsV1Response(**response_data) + + assert response.tags is not None + assert len(response.tags) == 5 + assert "tag-with-dashes" in response.tags + assert "tag/with/slashes" in response.tags + + def test_billing_fields_response_with_unicode_in_line_items(self): + """Test billing fields response with unicode characters.""" + response_data = { + "line_items": { + "streaming::nova-3": "Nova-3 Streaming 🚀", + "batch::model-测试": "Test Model 测试", + "streaming::émoji": "Émoji Model with accénts", + } + } + + response = ListBillingFieldsV1Response(**response_data) + + assert response.line_items is not None + assert response.line_items["streaming::nova-3"] == "Nova-3 Streaming 🚀" + assert response.line_items["batch::model-测试"] == "Test Model 测试" + assert response.line_items["streaming::émoji"] == "Émoji Model with accénts" + + def test_billing_fields_response_comparison(self): + """Test billing fields response equality comparison.""" + response_data = { + "accessors": ["12345678-1234-1234-1234-123456789012"], + "deployments": ["hosted"], + "tags": ["tag1"], + "line_items": {"streaming::nova-3": "Nova-3 Streaming"}, + } + + response1 = ListBillingFieldsV1Response(**response_data) + response2 = ListBillingFieldsV1Response(**response_data) + + # Same data should be equal + assert response1 == response2 + + # Different data should not be equal + different_data = response_data.copy() + different_data["accessors"] = ["87654321-4321-4321-4321-210987654321"] + response3 = ListBillingFieldsV1Response(**different_data) + assert response1 != response3 + + +class TestListBillingFieldsV1ResponseDeploymentsItem: + """Test ListBillingFieldsV1ResponseDeploymentsItem type.""" + + def test_deployments_item_literal_values(self): + """Test that deployments item accepts literal values.""" + valid_deployments = ["hosted", "beta", "self-hosted", "dedicated"] + + for deployment in valid_deployments: + deployment_value: ListBillingFieldsV1ResponseDeploymentsItem = deployment + assert isinstance(deployment_value, str) + + def test_deployments_item_custom_value(self): + """Test that deployments item accepts custom values due to typing.Any.""" + # String not in literals + custom_deployment: ListBillingFieldsV1ResponseDeploymentsItem = ( + "custom-deployment" + ) + assert isinstance(custom_deployment, str) + assert custom_deployment == "custom-deployment" + + def test_deployments_item_in_response(self): + """Test deployments item usage within a response.""" + response_data = { + "deployments": ["hosted", "beta", "custom-deployment", "self-hosted"] + } + + response = ListBillingFieldsV1Response(**response_data) + + assert response.deployments is not None + assert len(response.deployments) == 4 + assert "hosted" in response.deployments + assert "custom-deployment" in response.deployments + + +class TestBillingFieldsResponseIntegration: + """Integration tests for billing fields response models.""" + + def test_realistic_billing_fields_response(self): + """Test a realistic billing fields response with typical data.""" + response_data = { + "accessors": [ + "a1b2c3d4-5678-90ab-cdef-1234567890ab", + "b2c3d4e5-6789-01bc-def0-234567890abc", + "c3d4e5f6-7890-12cd-ef01-34567890abcd", + ], + "deployments": ["hosted", "self-hosted"], + "tags": [ + "production", + "customer-123", + "region-us-east", + "team-engineering", + ], + "line_items": { + "streaming::nova-3": "Nova-3 Streaming Transcription", + "streaming::nova-2": "Nova-2 Streaming Transcription", + "batch::nova-3": "Nova-3 Batch Transcription", + "batch::whisper": "Whisper Batch Transcription", + "streaming::enhanced": "Enhanced Streaming Transcription", + "tts::aura": "Aura Text-to-Speech", + }, + } + + response = ListBillingFieldsV1Response(**response_data) + + # Verify all fields are present and correct + assert len(response.accessors) == 3 + assert len(response.deployments) == 2 + assert len(response.tags) == 4 + assert len(response.line_items) == 6 + + # Verify specific values + assert "customer-123" in response.tags + assert "hosted" in response.deployments + assert response.line_items["tts::aura"] == "Aura Text-to-Speech" + + def test_billing_fields_response_with_date_filters(self): + """Test billing fields response scenario with date-filtered data.""" + # This represents a response for a specific date range + response_data = { + "accessors": ["12345678-1234-1234-1234-123456789012"], + "deployments": ["hosted"], + "tags": ["q1-2024", "january"], + "line_items": { + "streaming::nova-3": "Nova-3 Streaming", + "batch::nova-2": "Nova-2 Batch", + }, + } + + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors is not None + assert len(response.accessors) == 1 + assert "q1-2024" in response.tags + assert len(response.line_items) == 2 + + def test_billing_fields_response_empty_results(self): + """Test billing fields response with no data for the period.""" + # This represents a response for a period with no billing data + response_data = { + "accessors": [], + "deployments": [], + "tags": [], + "line_items": {}, + } + + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors == [] + assert response.deployments == [] + assert response.tags == [] + assert response.line_items == {} + + def test_billing_fields_response_partial_data(self): + """Test billing fields response with partial data.""" + # Some projects might only have certain fields populated + response_data = { + "deployments": ["hosted"], + "line_items": {"streaming::nova-3": "Nova-3 Streaming"}, + } + + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors is None + assert response.deployments is not None + assert response.tags is None + assert response.line_items is not None + + def test_multiple_billing_fields_responses_comparison(self): + """Test comparing multiple billing fields responses.""" + response1_data = { + "accessors": ["12345678-1234-1234-1234-123456789012"], + "tags": ["january"], + } + + response2_data = { + "accessors": [ + "12345678-1234-1234-1234-123456789012", + "87654321-4321-4321-4321-210987654321", + ], + "tags": ["february"], + } + + response1 = ListBillingFieldsV1Response(**response1_data) + response2 = ListBillingFieldsV1Response(**response2_data) + + # Verify they are different + assert response1 != response2 + assert len(response1.accessors) == 1 + assert len(response2.accessors) == 2 + + def test_billing_fields_response_model_evolution(self): + """Test that the model handles potential future fields gracefully.""" + # Simulate a response with additional fields that might be added in the future + response_data = { + "accessors": ["12345678-1234-1234-1234-123456789012"], + "deployments": ["hosted"], + "tags": ["tag1"], + "line_items": {"streaming::nova-3": "Nova-3 Streaming"}, + # Future fields + "future_field_1": "some_value", + "future_field_2": {"nested": "data"}, + "future_field_3": [1, 2, 3], + } + + # Should not raise an error due to extra="allow" + response = ListBillingFieldsV1Response(**response_data) + + assert response.accessors is not None + assert response.deployments is not None + assert response.tags is not None + assert response.line_items is not None + assert hasattr(response, "future_field_1") + assert hasattr(response, "future_field_2") + assert hasattr(response, "future_field_3") +