diff --git a/HISTORY.md b/HISTORY.md index 54314d6..5dddb45 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,22 @@ ## Unreleased +### Destinations: batch secret rotation + +New method on `client.destinations`: + +- `batch_update(team_id, *, destination_type=, updates=)` — rotate secrets for + up to 100 destinations of the same type in a single request. Each update is + applied independently; partial failures are reported per item with + `has_errors`, `error_code` and `message`. + +Available on sync and async clients, mirrored under `with_raw_response`, +and takes the usual `auth_token` / `headers` / `timeout` overrides. + +`BatchUpdateDestinationsBodyUpdatesItem` is exported from `supermetrics` for +convenience: +`from supermetrics import BatchUpdateDestinationsBodyUpdatesItem`. + ### Table Groups: list, export, import, edit New `client.table_groups` resource with four methods: diff --git a/README.md b/README.md index efd6f6e..35a0354 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Official Python client for Supermetrics * Fully typed request and response models, generated from the spec as `attrs` classes * Comprehensive API coverage: login links (including update), logins (including account listing and revocation), accounts, queries, DWH transfers (including clone and batch - create) and transfer runs, DWH destinations, DWH table groups (list, export, import, + create) and transfer runs, DWH destinations (including batch secret rotation), DWH table groups (list, export, import, edit), DWH backfills, custom fields, data blending, account tags, Connector Builder * Custom exception hierarchy with HTTP status code mapping * Resource-based API organization @@ -449,6 +449,22 @@ if usage.is_used: print(f"still used by {transfer.transfer_id}: {transfer.transfer_name}") else: client.destinations.delete(team_id=12345, destination_id=8) + +# Rotate secrets for multiple destinations of the same type in one call +from supermetrics import BatchUpdateDestinationsBodyUpdatesItem + +results = client.destinations.batch_update( + team_id=12345, + destination_type="DWH_SNOWFLAKE", + updates=[ + BatchUpdateDestinationsBodyUpdatesItem(destination_id=8, new_secret="not-a-real-new-password"), + BatchUpdateDestinationsBodyUpdatesItem(destination_id=9, new_secret="not-a-real-new-password"), + ], +) +if results.has_errors: + for item in results.results: + if item.status == "error": + print(f" destination {item.destination_id} failed: {item.error_code}") ``` ### Data Warehouse Table Groups diff --git a/docs/api-reference.md b/docs/api-reference.md index 9617c66..bdfc050 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -2621,7 +2621,49 @@ else: client.destinations.delete(team_id=12345, destination_id=8) ``` -**Async usage** (all seven methods above are also available on +#### batch_update() + +Rotate secrets for multiple destinations of the same type in a single request. +Each update is applied independently — if one fails, the others still succeed. + +```python +from supermetrics import BatchUpdateDestinationsBodyUpdatesItem + +results = client.destinations.batch_update( + team_id=12345, + destination_type="DWH_SNOWFLAKE", + updates=[ + BatchUpdateDestinationsBodyUpdatesItem(destination_id=8, new_secret="not-a-real-new-password"), + BatchUpdateDestinationsBodyUpdatesItem(destination_id=9, new_secret="not-a-real-new-password"), + ], +) +``` + +**Parameters:** + +- `team_id` (int, required): Unique identifier of the team +- `destination_type` (str, required): Destination type shared by all items in the batch + (e.g. `"DWH_SNOWFLAKE"`) +- `updates` (list[BatchUpdateDestinationsBodyUpdatesItem], required): Secret rotations to + apply — each carries `destination_id` and `new_secret`. Between 1 and 100 items; + duplicates are rejected. + +**Returns:** `BatchUpdateDestinationsResponse200Data` with `has_errors` (bool) and +`results`, a list of items carrying `destination_id`, `status` (`"success"` or `"error"`), +and, on failure, `error_code` and `message`. + +**Raises:** `SupermetricsAuthError` (401), `SupermetricsForbiddenError` (403), `SupermetricsValidationError` (400), `SupermetricsRateLimitError` (429), `SupermetricsServerError` (500), `NetworkError` + +**Example:** + +```python +if results.has_errors: + for item in results.results: + if item.status == "error": + print(f" destination {item.destination_id} failed: {item.error_code}") +``` + +**Async usage** (all eight methods above are also available on `DestinationsAsyncResource`): ```python diff --git a/openapi-spec.yaml b/openapi-spec.yaml index 4be9dfc..16a72b6 100644 --- a/openapi-spec.yaml +++ b/openapi-spec.yaml @@ -1568,6 +1568,121 @@ paths: $ref: '#/components/responses/InternalServerError' security: - ApiKeyAuth: [] + /teams/{team_id}/destinations/batch: + patch: + summary: Batch rotate destination secrets + description: 'Rotate secrets for multiple destinations of the same type in a + single request. + + Each update is applied independently — if one fails, the others still succeed. + + + Batch-level validations (checked before any processing): + + - The updates array must contain between 1 and 100 items. + + - Each item must include a valid destination_id and a non-empty new_secret. + + - Duplicate destination_id values are not allowed. + + ' + operationId: batchUpdateDestinations + tags: + - Data Destinations + requestBody: + description: Batch of secret rotations sharing a single destination type + required: true + content: + application/json: + schema: + type: object + required: + - type + - updates + properties: + type: + type: string + description: Destination type shared by all items in the batch + example: DWH_SNOWFLAKE + updates: + type: array + minItems: 1 + maxItems: 100 + items: + type: object + required: + - destination_id + - new_secret + properties: + destination_id: + type: integer + description: ID of the destination to rotate the secret for + new_secret: + type: string + description: New secret value for credential rotation + responses: + '200': + description: Batch processed + headers: + Access-Control-Allow-Origin: + $ref: '#/components/headers/Access-Control-Allow-Origin' + content: + application/json: + schema: + type: object + properties: + meta: + $ref: '#/components/schemas/Meta' + data: + type: object + required: + - has_errors + - results + properties: + has_errors: + type: boolean + description: True if any item in the batch failed. Allows + quick failure detection without iterating all results. + results: + type: array + items: + type: object + required: + - destination_id + - status + properties: + destination_id: + type: integer + status: + type: string + enum: + - success + - error + error_code: + type: string + description: Error code identifying the failure reason. + Only present when status is error. + message: + type: string + description: Human-readable error description. Only + present when status is error. + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/InternalServerError' + security: + - ApiKeyAuth: [] + parameters: + - $ref: '#/components/parameters/TeamId' + servers: + - url: https://dts-api.supermetrics.com/v1 + description: Global production public Supermetrics Data Warehouse Destinations + API base path. + x-internal: false /teams/{team_id}/destinations/test-connection: post: summary: Test destination connection diff --git a/scripts/references/sdk-endpoint-filters.yaml b/scripts/references/sdk-endpoint-filters.yaml index 2bcbafc..4d3cd64 100644 --- a/scripts/references/sdk-endpoint-filters.yaml +++ b/scripts/references/sdk-endpoint-filters.yaml @@ -183,6 +183,8 @@ endpoints: path: /teams/{team_id}/destinations/test-connection - method: GET path: /teams/{team_id}/destinations/{destination_id}/usage + - method: PATCH + path: /teams/{team_id}/destinations/batch # Table Groups # diff --git a/src/supermetrics/__init__.py b/src/supermetrics/__init__.py index 61655bd..3f5c6d0 100644 --- a/src/supermetrics/__init__.py +++ b/src/supermetrics/__init__.py @@ -2,6 +2,9 @@ from supermetrics.__version__ import __version__ from supermetrics._auth import AsyncTokenProvider, TokenProvider +from supermetrics._generated.supermetrics_api_client.models.batch_update_destinations_body_updates_item import ( + BatchUpdateDestinationsBodyUpdatesItem, +) from supermetrics._generated.supermetrics_api_client.models.blend_config import BlendConfig from supermetrics._generated.supermetrics_api_client.models.blend_config_query_table import BlendConfigQueryTable from supermetrics._generated.supermetrics_api_client.models.blend_datasource_field_ref import BlendDatasourceFieldRef @@ -107,6 +110,8 @@ "TransferDataSourceSetting", "CloneTransferBody", "TransferConfigurationRequest", + # Destination batch update. batch_update() takes a list of these. + "BatchUpdateDestinationsBodyUpdatesItem", # Table group request models. import_ and edit take these as the body payload. "ImportTableGroupBody", "EditTableGroupBody", diff --git a/src/supermetrics/_generated/supermetrics_api_client/api/data_destinations/batch_update_destinations.py b/src/supermetrics/_generated/supermetrics_api_client/api/data_destinations/batch_update_destinations.py new file mode 100644 index 0000000..61501cd --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/api/data_destinations/batch_update_destinations.py @@ -0,0 +1,271 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.batch_update_destinations_body import BatchUpdateDestinationsBody +from ...models.batch_update_destinations_response_200 import BatchUpdateDestinationsResponse200 +from ...models.batch_update_destinations_response_400 import BatchUpdateDestinationsResponse400 +from ...models.batch_update_destinations_response_401 import BatchUpdateDestinationsResponse401 +from ...models.batch_update_destinations_response_403 import BatchUpdateDestinationsResponse403 +from ...models.batch_update_destinations_response_500 import BatchUpdateDestinationsResponse500 +from ...types import Response + + +def _get_kwargs( + team_id: int, + *, + body: BatchUpdateDestinationsBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "patch", + "url": "/teams/{team_id}/destinations/batch".format( + team_id=quote(str(team_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ( + BatchUpdateDestinationsResponse200 + | BatchUpdateDestinationsResponse400 + | BatchUpdateDestinationsResponse401 + | BatchUpdateDestinationsResponse403 + | BatchUpdateDestinationsResponse500 + | None +): + if response.status_code == 200: + response_200 = BatchUpdateDestinationsResponse200.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = BatchUpdateDestinationsResponse400.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = BatchUpdateDestinationsResponse401.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = BatchUpdateDestinationsResponse403.from_dict(response.json()) + + return response_403 + + if response.status_code == 500: + response_500 = BatchUpdateDestinationsResponse500.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ + BatchUpdateDestinationsResponse200 + | BatchUpdateDestinationsResponse400 + | BatchUpdateDestinationsResponse401 + | BatchUpdateDestinationsResponse403 + | BatchUpdateDestinationsResponse500 +]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + team_id: int, + *, + client: AuthenticatedClient, + body: BatchUpdateDestinationsBody, +) -> Response[ + BatchUpdateDestinationsResponse200 + | BatchUpdateDestinationsResponse400 + | BatchUpdateDestinationsResponse401 + | BatchUpdateDestinationsResponse403 + | BatchUpdateDestinationsResponse500 +]: + """Batch rotate destination secrets + + Rotate secrets for multiple destinations of the same type in a single request. + Each update is applied independently — if one fails, the others still succeed. + + Batch-level validations (checked before any processing): + - The updates array must contain between 1 and 100 items. + - Each item must include a valid destination_id and a non-empty new_secret. + - Duplicate destination_id values are not allowed. + + Args: + team_id (int): + body (BatchUpdateDestinationsBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[BatchUpdateDestinationsResponse200 | BatchUpdateDestinationsResponse400 | BatchUpdateDestinationsResponse401 | BatchUpdateDestinationsResponse403 | BatchUpdateDestinationsResponse500] + """ + + kwargs = _get_kwargs( + team_id=team_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + team_id: int, + *, + client: AuthenticatedClient, + body: BatchUpdateDestinationsBody, +) -> ( + BatchUpdateDestinationsResponse200 + | BatchUpdateDestinationsResponse400 + | BatchUpdateDestinationsResponse401 + | BatchUpdateDestinationsResponse403 + | BatchUpdateDestinationsResponse500 + | None +): + """Batch rotate destination secrets + + Rotate secrets for multiple destinations of the same type in a single request. + Each update is applied independently — if one fails, the others still succeed. + + Batch-level validations (checked before any processing): + - The updates array must contain between 1 and 100 items. + - Each item must include a valid destination_id and a non-empty new_secret. + - Duplicate destination_id values are not allowed. + + Args: + team_id (int): + body (BatchUpdateDestinationsBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + BatchUpdateDestinationsResponse200 | BatchUpdateDestinationsResponse400 | BatchUpdateDestinationsResponse401 | BatchUpdateDestinationsResponse403 | BatchUpdateDestinationsResponse500 + """ + + return sync_detailed( + team_id=team_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + team_id: int, + *, + client: AuthenticatedClient, + body: BatchUpdateDestinationsBody, +) -> Response[ + BatchUpdateDestinationsResponse200 + | BatchUpdateDestinationsResponse400 + | BatchUpdateDestinationsResponse401 + | BatchUpdateDestinationsResponse403 + | BatchUpdateDestinationsResponse500 +]: + """Batch rotate destination secrets + + Rotate secrets for multiple destinations of the same type in a single request. + Each update is applied independently — if one fails, the others still succeed. + + Batch-level validations (checked before any processing): + - The updates array must contain between 1 and 100 items. + - Each item must include a valid destination_id and a non-empty new_secret. + - Duplicate destination_id values are not allowed. + + Args: + team_id (int): + body (BatchUpdateDestinationsBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[BatchUpdateDestinationsResponse200 | BatchUpdateDestinationsResponse400 | BatchUpdateDestinationsResponse401 | BatchUpdateDestinationsResponse403 | BatchUpdateDestinationsResponse500] + """ + + kwargs = _get_kwargs( + team_id=team_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + team_id: int, + *, + client: AuthenticatedClient, + body: BatchUpdateDestinationsBody, +) -> ( + BatchUpdateDestinationsResponse200 + | BatchUpdateDestinationsResponse400 + | BatchUpdateDestinationsResponse401 + | BatchUpdateDestinationsResponse403 + | BatchUpdateDestinationsResponse500 + | None +): + """Batch rotate destination secrets + + Rotate secrets for multiple destinations of the same type in a single request. + Each update is applied independently — if one fails, the others still succeed. + + Batch-level validations (checked before any processing): + - The updates array must contain between 1 and 100 items. + - Each item must include a valid destination_id and a non-empty new_secret. + - Duplicate destination_id values are not allowed. + + Args: + team_id (int): + body (BatchUpdateDestinationsBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + BatchUpdateDestinationsResponse200 | BatchUpdateDestinationsResponse400 | BatchUpdateDestinationsResponse401 | BatchUpdateDestinationsResponse403 | BatchUpdateDestinationsResponse500 + """ + + return ( + await asyncio_detailed( + team_id=team_id, + client=client, + body=body, + ) + ).parsed diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/__init__.py b/src/supermetrics/_generated/supermetrics_api_client/models/__init__.py index 916c0c9..816e436 100644 --- a/src/supermetrics/_generated/supermetrics_api_client/models/__init__.py +++ b/src/supermetrics/_generated/supermetrics_api_client/models/__init__.py @@ -39,6 +39,22 @@ from .batch_create_transfers_response_403_meta import BatchCreateTransfersResponse403Meta from .batch_create_transfers_response_500 import BatchCreateTransfersResponse500 from .batch_create_transfers_response_500_meta import BatchCreateTransfersResponse500Meta +from .batch_update_destinations_body import BatchUpdateDestinationsBody +from .batch_update_destinations_body_updates_item import BatchUpdateDestinationsBodyUpdatesItem +from .batch_update_destinations_response_200 import BatchUpdateDestinationsResponse200 +from .batch_update_destinations_response_200_data import BatchUpdateDestinationsResponse200Data +from .batch_update_destinations_response_200_data_results_item import BatchUpdateDestinationsResponse200DataResultsItem +from .batch_update_destinations_response_200_data_results_item_status import ( + BatchUpdateDestinationsResponse200DataResultsItemStatus, +) +from .batch_update_destinations_response_400 import BatchUpdateDestinationsResponse400 +from .batch_update_destinations_response_400_meta import BatchUpdateDestinationsResponse400Meta +from .batch_update_destinations_response_401 import BatchUpdateDestinationsResponse401 +from .batch_update_destinations_response_401_meta import BatchUpdateDestinationsResponse401Meta +from .batch_update_destinations_response_403 import BatchUpdateDestinationsResponse403 +from .batch_update_destinations_response_403_meta import BatchUpdateDestinationsResponse403Meta +from .batch_update_destinations_response_500 import BatchUpdateDestinationsResponse500 +from .batch_update_destinations_response_500_meta import BatchUpdateDestinationsResponse500Meta from .blend_base_request import BlendBaseRequest from .blend_config import BlendConfig from .blend_config_output import BlendConfigOutput @@ -1130,6 +1146,20 @@ "BatchCreateTransfersResponse403Meta", "BatchCreateTransfersResponse500", "BatchCreateTransfersResponse500Meta", + "BatchUpdateDestinationsBody", + "BatchUpdateDestinationsBodyUpdatesItem", + "BatchUpdateDestinationsResponse200", + "BatchUpdateDestinationsResponse200Data", + "BatchUpdateDestinationsResponse200DataResultsItem", + "BatchUpdateDestinationsResponse200DataResultsItemStatus", + "BatchUpdateDestinationsResponse400", + "BatchUpdateDestinationsResponse400Meta", + "BatchUpdateDestinationsResponse401", + "BatchUpdateDestinationsResponse401Meta", + "BatchUpdateDestinationsResponse403", + "BatchUpdateDestinationsResponse403Meta", + "BatchUpdateDestinationsResponse500", + "BatchUpdateDestinationsResponse500Meta", "BlendBaseRequest", "BlendConfig", "BlendConfigOutput", diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_body.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_body.py new file mode 100644 index 0000000..53d282d --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_body.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.batch_update_destinations_body_updates_item import BatchUpdateDestinationsBodyUpdatesItem + + +T = TypeVar("T", bound="BatchUpdateDestinationsBody") + + +@_attrs_define +class BatchUpdateDestinationsBody: + """ + Attributes: + type_ (str): Destination type shared by all items in the batch Example: DWH_SNOWFLAKE. + updates (list[BatchUpdateDestinationsBodyUpdatesItem]): + """ + + type_: str + updates: list[BatchUpdateDestinationsBodyUpdatesItem] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + type_ = self.type_ + + updates = [] + for updates_item_data in self.updates: + updates_item = updates_item_data.to_dict() + updates.append(updates_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "type": type_, + "updates": updates, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.batch_update_destinations_body_updates_item import BatchUpdateDestinationsBodyUpdatesItem + + d = dict(src_dict) + type_ = d.pop("type") + + updates = [] + _updates = d.pop("updates") + for updates_item_data in _updates: + updates_item = BatchUpdateDestinationsBodyUpdatesItem.from_dict(updates_item_data) + + updates.append(updates_item) + + batch_update_destinations_body = cls( + type_=type_, + updates=updates, + ) + + batch_update_destinations_body.additional_properties = d + return batch_update_destinations_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_body_updates_item.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_body_updates_item.py new file mode 100644 index 0000000..bf51cd1 --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_body_updates_item.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="BatchUpdateDestinationsBodyUpdatesItem") + + +@_attrs_define +class BatchUpdateDestinationsBodyUpdatesItem: + """ + Attributes: + destination_id (int): ID of the destination to rotate the secret for + new_secret (str): New secret value for credential rotation + """ + + destination_id: int + new_secret: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + destination_id = self.destination_id + + new_secret = self.new_secret + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "destination_id": destination_id, + "new_secret": new_secret, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + destination_id = d.pop("destination_id") + + new_secret = d.pop("new_secret") + + batch_update_destinations_body_updates_item = cls( + destination_id=destination_id, + new_secret=new_secret, + ) + + batch_update_destinations_body_updates_item.additional_properties = d + return batch_update_destinations_body_updates_item + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200.py new file mode 100644 index 0000000..bd1fd49 --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.batch_update_destinations_response_200_data import BatchUpdateDestinationsResponse200Data + from ..models.meta import Meta + + +T = TypeVar("T", bound="BatchUpdateDestinationsResponse200") + + +@_attrs_define +class BatchUpdateDestinationsResponse200: + """ + Attributes: + meta (Meta | Unset): Metadata included in every API response. + data (BatchUpdateDestinationsResponse200Data | Unset): + """ + + meta: Meta | Unset = UNSET + data: BatchUpdateDestinationsResponse200Data | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + meta: dict[str, Any] | Unset = UNSET + if not isinstance(self.meta, Unset): + meta = self.meta.to_dict() + + data: dict[str, Any] | Unset = UNSET + if not isinstance(self.data, Unset): + data = self.data.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if meta is not UNSET: + field_dict["meta"] = meta + if data is not UNSET: + field_dict["data"] = data + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.batch_update_destinations_response_200_data import BatchUpdateDestinationsResponse200Data + from ..models.meta import Meta + + d = dict(src_dict) + _meta = d.pop("meta", UNSET) + meta: Meta | Unset + if isinstance(_meta, Unset): + meta = UNSET + else: + meta = Meta.from_dict(_meta) + + _data = d.pop("data", UNSET) + data: BatchUpdateDestinationsResponse200Data | Unset + if isinstance(_data, Unset): + data = UNSET + else: + data = BatchUpdateDestinationsResponse200Data.from_dict(_data) + + batch_update_destinations_response_200 = cls( + meta=meta, + data=data, + ) + + batch_update_destinations_response_200.additional_properties = d + return batch_update_destinations_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200_data.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200_data.py new file mode 100644 index 0000000..303edc1 --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200_data.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.batch_update_destinations_response_200_data_results_item import ( + BatchUpdateDestinationsResponse200DataResultsItem, + ) + + +T = TypeVar("T", bound="BatchUpdateDestinationsResponse200Data") + + +@_attrs_define +class BatchUpdateDestinationsResponse200Data: + """ + Attributes: + has_errors (bool): True if any item in the batch failed. Allows quick failure detection without iterating all + results. + results (list[BatchUpdateDestinationsResponse200DataResultsItem]): + """ + + has_errors: bool + results: list[BatchUpdateDestinationsResponse200DataResultsItem] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + has_errors = self.has_errors + + results = [] + for results_item_data in self.results: + results_item = results_item_data.to_dict() + results.append(results_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "has_errors": has_errors, + "results": results, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.batch_update_destinations_response_200_data_results_item import ( + BatchUpdateDestinationsResponse200DataResultsItem, + ) + + d = dict(src_dict) + has_errors = d.pop("has_errors") + + results = [] + _results = d.pop("results") + for results_item_data in _results: + results_item = BatchUpdateDestinationsResponse200DataResultsItem.from_dict(results_item_data) + + results.append(results_item) + + batch_update_destinations_response_200_data = cls( + has_errors=has_errors, + results=results, + ) + + batch_update_destinations_response_200_data.additional_properties = d + return batch_update_destinations_response_200_data + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200_data_results_item.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200_data_results_item.py new file mode 100644 index 0000000..335a2cc --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200_data_results_item.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.batch_update_destinations_response_200_data_results_item_status import ( + BatchUpdateDestinationsResponse200DataResultsItemStatus, + check_batch_update_destinations_response_200_data_results_item_status, +) +from ..types import UNSET, Unset + +T = TypeVar("T", bound="BatchUpdateDestinationsResponse200DataResultsItem") + + +@_attrs_define +class BatchUpdateDestinationsResponse200DataResultsItem: + """ + Attributes: + destination_id (int): + status (BatchUpdateDestinationsResponse200DataResultsItemStatus): + error_code (str | Unset): Error code identifying the failure reason. Only present when status is error. + message (str | Unset): Human-readable error description. Only present when status is error. + """ + + destination_id: int + status: BatchUpdateDestinationsResponse200DataResultsItemStatus + error_code: str | Unset = UNSET + message: str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + destination_id = self.destination_id + + status: str = self.status + + error_code = self.error_code + + message = self.message + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "destination_id": destination_id, + "status": status, + } + ) + if error_code is not UNSET: + field_dict["error_code"] = error_code + if message is not UNSET: + field_dict["message"] = message + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + destination_id = d.pop("destination_id") + + status = check_batch_update_destinations_response_200_data_results_item_status(d.pop("status")) + + error_code = d.pop("error_code", UNSET) + + message = d.pop("message", UNSET) + + batch_update_destinations_response_200_data_results_item = cls( + destination_id=destination_id, + status=status, + error_code=error_code, + message=message, + ) + + batch_update_destinations_response_200_data_results_item.additional_properties = d + return batch_update_destinations_response_200_data_results_item + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200_data_results_item_status.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200_data_results_item_status.py new file mode 100644 index 0000000..10c1b42 --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_200_data_results_item_status.py @@ -0,0 +1,20 @@ +from typing import Literal + +BatchUpdateDestinationsResponse200DataResultsItemStatus = Literal["error", "success"] + +BATCH_UPDATE_DESTINATIONS_RESPONSE_200_DATA_RESULTS_ITEM_STATUS_VALUES: set[ + BatchUpdateDestinationsResponse200DataResultsItemStatus +] = { + "error", + "success", +} + + +def check_batch_update_destinations_response_200_data_results_item_status( + value: str, +) -> BatchUpdateDestinationsResponse200DataResultsItemStatus: + if value in BATCH_UPDATE_DESTINATIONS_RESPONSE_200_DATA_RESULTS_ITEM_STATUS_VALUES: + return value + raise TypeError( + f"Unexpected value {value!r}. Expected one of {BATCH_UPDATE_DESTINATIONS_RESPONSE_200_DATA_RESULTS_ITEM_STATUS_VALUES!r}" + ) diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_400.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_400.py new file mode 100644 index 0000000..5828809 --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_400.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +if TYPE_CHECKING: + from ..models.batch_update_destinations_response_400_meta import BatchUpdateDestinationsResponse400Meta + from ..models.error import Error + + +T = TypeVar("T", bound="BatchUpdateDestinationsResponse400") + + +@_attrs_define +class BatchUpdateDestinationsResponse400: + """Standard envelope returned by all error (4xx/5xx) responses. + + Attributes: + meta (BatchUpdateDestinationsResponse400Meta): Metadata included in every API response. + error (Error): Machine- and human-readable detail for a failed request. + """ + + meta: BatchUpdateDestinationsResponse400Meta + error: Error + + def to_dict(self) -> dict[str, Any]: + meta = self.meta.to_dict() + + error = self.error.to_dict() + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "meta": meta, + "error": error, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.batch_update_destinations_response_400_meta import BatchUpdateDestinationsResponse400Meta + from ..models.error import Error + + d = dict(src_dict) + meta = BatchUpdateDestinationsResponse400Meta.from_dict(d.pop("meta")) + + error = Error.from_dict(d.pop("error")) + + batch_update_destinations_response_400 = cls( + meta=meta, + error=error, + ) + + return batch_update_destinations_response_400 diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_400_meta.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_400_meta.py new file mode 100644 index 0000000..f6ce16b --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_400_meta.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="BatchUpdateDestinationsResponse400Meta") + + +@_attrs_define +class BatchUpdateDestinationsResponse400Meta: + """Metadata included in every API response. + + Attributes: + request_id (str): Unique identifier for the request, for tracking and debugging. Example: + BXaEFVtjc7TXaJxgZhmFgSUD9edqq_CN. + """ + + request_id: str + + def to_dict(self) -> dict[str, Any]: + request_id = self.request_id + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "request_id": request_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + request_id = d.pop("request_id") + + batch_update_destinations_response_400_meta = cls( + request_id=request_id, + ) + + return batch_update_destinations_response_400_meta diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_401.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_401.py new file mode 100644 index 0000000..c5314be --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_401.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +if TYPE_CHECKING: + from ..models.batch_update_destinations_response_401_meta import BatchUpdateDestinationsResponse401Meta + from ..models.error import Error + + +T = TypeVar("T", bound="BatchUpdateDestinationsResponse401") + + +@_attrs_define +class BatchUpdateDestinationsResponse401: + """Standard envelope returned by all error (4xx/5xx) responses. + + Attributes: + meta (BatchUpdateDestinationsResponse401Meta): Metadata included in every API response. + error (Error): Machine- and human-readable detail for a failed request. + """ + + meta: BatchUpdateDestinationsResponse401Meta + error: Error + + def to_dict(self) -> dict[str, Any]: + meta = self.meta.to_dict() + + error = self.error.to_dict() + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "meta": meta, + "error": error, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.batch_update_destinations_response_401_meta import BatchUpdateDestinationsResponse401Meta + from ..models.error import Error + + d = dict(src_dict) + meta = BatchUpdateDestinationsResponse401Meta.from_dict(d.pop("meta")) + + error = Error.from_dict(d.pop("error")) + + batch_update_destinations_response_401 = cls( + meta=meta, + error=error, + ) + + return batch_update_destinations_response_401 diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_401_meta.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_401_meta.py new file mode 100644 index 0000000..024ee1d --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_401_meta.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="BatchUpdateDestinationsResponse401Meta") + + +@_attrs_define +class BatchUpdateDestinationsResponse401Meta: + """Metadata included in every API response. + + Attributes: + request_id (str): Unique identifier for the request, for tracking and debugging. Example: + BXaEFVtjc7TXaJxgZhmFgSUD9edqq_CN. + """ + + request_id: str + + def to_dict(self) -> dict[str, Any]: + request_id = self.request_id + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "request_id": request_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + request_id = d.pop("request_id") + + batch_update_destinations_response_401_meta = cls( + request_id=request_id, + ) + + return batch_update_destinations_response_401_meta diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_403.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_403.py new file mode 100644 index 0000000..2a84a9c --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_403.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +if TYPE_CHECKING: + from ..models.batch_update_destinations_response_403_meta import BatchUpdateDestinationsResponse403Meta + from ..models.error import Error + + +T = TypeVar("T", bound="BatchUpdateDestinationsResponse403") + + +@_attrs_define +class BatchUpdateDestinationsResponse403: + """Standard envelope returned by all error (4xx/5xx) responses. + + Attributes: + meta (BatchUpdateDestinationsResponse403Meta): Metadata included in every API response. + error (Error): Machine- and human-readable detail for a failed request. + """ + + meta: BatchUpdateDestinationsResponse403Meta + error: Error + + def to_dict(self) -> dict[str, Any]: + meta = self.meta.to_dict() + + error = self.error.to_dict() + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "meta": meta, + "error": error, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.batch_update_destinations_response_403_meta import BatchUpdateDestinationsResponse403Meta + from ..models.error import Error + + d = dict(src_dict) + meta = BatchUpdateDestinationsResponse403Meta.from_dict(d.pop("meta")) + + error = Error.from_dict(d.pop("error")) + + batch_update_destinations_response_403 = cls( + meta=meta, + error=error, + ) + + return batch_update_destinations_response_403 diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_403_meta.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_403_meta.py new file mode 100644 index 0000000..246a0a3 --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_403_meta.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="BatchUpdateDestinationsResponse403Meta") + + +@_attrs_define +class BatchUpdateDestinationsResponse403Meta: + """Metadata included in every API response. + + Attributes: + request_id (str): Unique identifier for the request, for tracking and debugging. Example: + BXaEFVtjc7TXaJxgZhmFgSUD9edqq_CN. + """ + + request_id: str + + def to_dict(self) -> dict[str, Any]: + request_id = self.request_id + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "request_id": request_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + request_id = d.pop("request_id") + + batch_update_destinations_response_403_meta = cls( + request_id=request_id, + ) + + return batch_update_destinations_response_403_meta diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_500.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_500.py new file mode 100644 index 0000000..57baa79 --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_500.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +if TYPE_CHECKING: + from ..models.batch_update_destinations_response_500_meta import BatchUpdateDestinationsResponse500Meta + from ..models.error import Error + + +T = TypeVar("T", bound="BatchUpdateDestinationsResponse500") + + +@_attrs_define +class BatchUpdateDestinationsResponse500: + """Standard envelope returned by all error (4xx/5xx) responses. + + Attributes: + meta (BatchUpdateDestinationsResponse500Meta): Metadata included in every API response. + error (Error): Machine- and human-readable detail for a failed request. + """ + + meta: BatchUpdateDestinationsResponse500Meta + error: Error + + def to_dict(self) -> dict[str, Any]: + meta = self.meta.to_dict() + + error = self.error.to_dict() + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "meta": meta, + "error": error, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.batch_update_destinations_response_500_meta import BatchUpdateDestinationsResponse500Meta + from ..models.error import Error + + d = dict(src_dict) + meta = BatchUpdateDestinationsResponse500Meta.from_dict(d.pop("meta")) + + error = Error.from_dict(d.pop("error")) + + batch_update_destinations_response_500 = cls( + meta=meta, + error=error, + ) + + return batch_update_destinations_response_500 diff --git a/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_500_meta.py b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_500_meta.py new file mode 100644 index 0000000..c4820ff --- /dev/null +++ b/src/supermetrics/_generated/supermetrics_api_client/models/batch_update_destinations_response_500_meta.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define + +T = TypeVar("T", bound="BatchUpdateDestinationsResponse500Meta") + + +@_attrs_define +class BatchUpdateDestinationsResponse500Meta: + """Metadata included in every API response. + + Attributes: + request_id (str): Unique identifier for the request, for tracking and debugging. Example: + BXaEFVtjc7TXaJxgZhmFgSUD9edqq_CN. + """ + + request_id: str + + def to_dict(self) -> dict[str, Any]: + request_id = self.request_id + + field_dict: dict[str, Any] = {} + + field_dict.update( + { + "request_id": request_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + request_id = d.pop("request_id") + + batch_update_destinations_response_500_meta = cls( + request_id=request_id, + ) + + return batch_update_destinations_response_500_meta diff --git a/src/supermetrics/resources/_raw.py b/src/supermetrics/resources/_raw.py index 47edd8b..3b5ac3c 100644 --- a/src/supermetrics/resources/_raw.py +++ b/src/supermetrics/resources/_raw.py @@ -238,6 +238,7 @@ def __init__(self, resource: DestinationsResource) -> None: self.delete = to_raw_response_wrapper(resource.delete) self.test_connection = to_raw_response_wrapper(resource.test_connection) self.get_usage = to_raw_response_wrapper(resource.get_usage) + self.batch_update = to_raw_response_wrapper(resource.batch_update) class DestinationsAsyncResourceWithRawResponse: @@ -256,6 +257,7 @@ def __init__(self, resource: DestinationsAsyncResource) -> None: self.delete = async_to_raw_response_wrapper(resource.delete) self.test_connection = async_to_raw_response_wrapper(resource.test_connection) self.get_usage = async_to_raw_response_wrapper(resource.get_usage) + self.batch_update = async_to_raw_response_wrapper(resource.batch_update) class LoginLinksResourceWithRawResponse: diff --git a/src/supermetrics/resources/destinations.py b/src/supermetrics/resources/destinations.py index 8d9eccf..ff69ef0 100644 --- a/src/supermetrics/resources/destinations.py +++ b/src/supermetrics/resources/destinations.py @@ -9,6 +9,7 @@ from supermetrics._generated.supermetrics_api_client import AuthenticatedClient from supermetrics._generated.supermetrics_api_client import Client as GeneratedClient from supermetrics._generated.supermetrics_api_client.api.data_destinations import ( + batch_update_destinations, create_destination, delete_destination, get_destination, @@ -17,6 +18,18 @@ test_connection, update_destination, ) +from supermetrics._generated.supermetrics_api_client.models.batch_update_destinations_body import ( + BatchUpdateDestinationsBody, +) +from supermetrics._generated.supermetrics_api_client.models.batch_update_destinations_body_updates_item import ( + BatchUpdateDestinationsBodyUpdatesItem, +) +from supermetrics._generated.supermetrics_api_client.models.batch_update_destinations_response_200 import ( + BatchUpdateDestinationsResponse200, +) +from supermetrics._generated.supermetrics_api_client.models.batch_update_destinations_response_200_data import ( + BatchUpdateDestinationsResponse200Data, +) from supermetrics._generated.supermetrics_api_client.models.create_destination_request import CreateDestinationRequest from supermetrics._generated.supermetrics_api_client.models.create_destination_request_fields import ( CreateDestinationRequestFields, @@ -47,6 +60,7 @@ # keeps ``list[DestinationListItem]`` in a later method meaning a list of destinations # rather than a subscript of ``DestinationsResource.list``. Do not inline these back. DestinationItemList = list[DestinationListItem] +BatchUpdateItemList = list[BatchUpdateDestinationsBodyUpdatesItem] FieldMap = dict[str, Any] @@ -451,6 +465,46 @@ async def get_usage( raw_body=response.content, ) + async def batch_update( + self, + team_id: int, + *, + destination_type: str, + updates: BatchUpdateItemList, + auth_token: str | None = None, + headers: dict[str, str] | None = None, + timeout: float | httpx.Timeout | None = None, + ) -> BatchUpdateDestinationsResponse200Data: + """Rotate secrets for multiple destinations in a single request. + + Async version of DestinationsResource.batch_update(). See sync version for full documentation. + """ + endpoint = f"/teams/{team_id}/destinations/batch" + with ( + api_error_handler(endpoint, context_400="Invalid batch update request"), + request_options(auth_token=auth_token, headers=headers, timeout=timeout), + ): + body = BatchUpdateDestinationsBody( + type_=destination_type, + updates=updates, + ) + response = await batch_update_destinations.asyncio_detailed( + client=cast(AuthenticatedClient, self._client), + team_id=team_id, + body=body, + ) + if response.status_code == 200: + parsed = cast(BatchUpdateDestinationsResponse200, response.parsed) + return cast(BatchUpdateDestinationsResponse200Data, parsed.data) + _raise_for_status( + int(response.status_code), + response.parsed, + endpoint, + bad_request_msg="Invalid batch update request", + headers=response.headers, + raw_body=response.content, + ) + class DestinationsResource: """Synchronous resource adapter for Data Warehouse Destination operations. @@ -1040,3 +1094,88 @@ def get_usage( headers=response.headers, raw_body=response.content, ) + + def batch_update( + self, + team_id: int, + *, + destination_type: str, + updates: BatchUpdateItemList, + auth_token: str | None = None, + headers: dict[str, str] | None = None, + timeout: float | httpx.Timeout | None = None, + ) -> BatchUpdateDestinationsResponse200Data: + """Rotate secrets for multiple destinations of the same type in a single request. + + Each update is applied independently — if one fails, the others still + succeed. The response carries a ``has_errors`` flag for quick failure + detection and a per-item ``results`` list with ``destination_id``, + ``status`` (``"success"`` or ``"error"``), and, on failure, ``error_code`` + and ``message``. + + Batch-level validations (checked before any processing): + - The updates list must contain between 1 and 100 items. + - Each item must include a valid ``destination_id`` and a non-empty + ``new_secret``. + - Duplicate ``destination_id`` values are not allowed. + + Args: + team_id: The unique identifier of the team. + destination_type: Destination type shared by all items in the batch + (e.g. ``"DWH_SNOWFLAKE"``). + updates: Secret rotations to apply — each a + ``BatchUpdateDestinationsBodyUpdatesItem(destination_id=..., + new_secret=...)``. + auth_token: Bearer token to use for this request only, overriding the + client credential. + headers: Extra HTTP headers for this request only. + timeout: Timeout override for this request only. + + Returns: + BatchUpdateDestinationsResponse200Data: Results with ``has_errors`` + flag and per-item ``results``. + + Raises: + AuthenticationError: If the API key is invalid or expired (HTTP 401). + ValidationError: If the batch request is invalid (HTTP 400). + APIError: If the API returns a server error (HTTP 403, 429, 5xx). + NetworkError: If a network error occurs during the request. + + Example: + >>> from supermetrics import BatchUpdateDestinationsBodyUpdatesItem + >>> results = client.destinations.batch_update( + ... team_id=12345, + ... destination_type="DWH_SNOWFLAKE", + ... updates=[ + ... BatchUpdateDestinationsBodyUpdatesItem( + ... destination_id=8, new_secret="new-password-123" + ... ), + ... ], + ... ) + >>> print(f"Errors: {results.has_errors}") + """ + endpoint = f"/teams/{team_id}/destinations/batch" + with ( + api_error_handler(endpoint, context_400="Invalid batch update request"), + request_options(auth_token=auth_token, headers=headers, timeout=timeout), + ): + body = BatchUpdateDestinationsBody( + type_=destination_type, + updates=updates, + ) + response = batch_update_destinations.sync_detailed( + client=cast(AuthenticatedClient, self._client), + team_id=team_id, + body=body, + ) + if response.status_code == 200: + parsed = cast(BatchUpdateDestinationsResponse200, response.parsed) + return cast(BatchUpdateDestinationsResponse200Data, parsed.data) + _raise_for_status( + int(response.status_code), + response.parsed, + endpoint, + bad_request_msg="Invalid batch update request", + headers=response.headers, + raw_body=response.content, + ) diff --git a/tests/e2e/test_destinations_e2e.py b/tests/e2e/test_destinations_e2e.py index 636b809..ac2a720 100644 --- a/tests/e2e/test_destinations_e2e.py +++ b/tests/e2e/test_destinations_e2e.py @@ -19,7 +19,7 @@ import pytest -from supermetrics import SupermetricsAsyncClient, SupermetricsClient +from supermetrics import BatchUpdateDestinationsBodyUpdatesItem, SupermetricsAsyncClient, SupermetricsClient from supermetrics._generated.supermetrics_api_client.models.destination_info import DestinationInfo from supermetrics._generated.supermetrics_api_client.models.destination_usage import DestinationUsage @@ -53,6 +53,7 @@ DESTINATION = f"{DESTINATIONS}/{DESTINATION_ID}" CONNECTION_TEST = f"{DESTINATIONS}/test-connection" USAGE = f"{DESTINATION}/usage" +BATCH = f"{DESTINATIONS}/batch" #: Every wrapped response carries this envelope metadata. META: dict[str, Any] = {"request_id": "req_0123456789ab"} @@ -146,6 +147,33 @@ } USAGE_UNUSED_BODY: dict[str, Any] = {"meta": META, "data": {"is_used": False, "transfers": []}} +#: PATCH .../destinations/batch — wrapped. +BATCH_SUCCESS_BODY: dict[str, Any] = { + "meta": META, + "data": { + "has_errors": False, + "results": [ + {"destination_id": 8, "status": "success"}, + {"destination_id": 9, "status": "success"}, + ], + }, +} +BATCH_PARTIAL_FAILURE_BODY: dict[str, Any] = { + "meta": META, + "data": { + "has_errors": True, + "results": [ + {"destination_id": 8, "status": "success"}, + { + "destination_id": 9, + "status": "error", + "error_code": "INVALID_SECRET", + "message": "Secret validation failed", + }, + ], + }, +} + def _configuration() -> dict[str, Any]: """Build the three required arguments shared by create, update and test_connection.""" @@ -158,7 +186,7 @@ def _error_envelope(code: str, message: str) -> dict[str, object]: class TestDestinationsResource: - """Synchronous destinations — all seven methods, both directions on the wire.""" + """Synchronous destinations — all eight methods, both directions on the wire.""" def test_list_unwraps_the_envelope_and_gets_the_collection(self, api_server: MockAPIServer) -> None: """The envelope is stripped and the items keep their flat list-item shape.""" @@ -412,6 +440,71 @@ def test_get_usage_reports_an_unused_destination(self, api_server: MockAPIServer assert request.path == USAGE assert request.bearer_token == "not-a-real-key" + # ── batch_update ──────────────────────────────────────────────────── + + def test_batch_update_sends_patch_and_unwraps_results(self, api_server: MockAPIServer) -> None: + """batch_update sends a PATCH with type and updates, and returns the data envelope.""" + api_server.route(BATCH, ScriptedResponse(json_body=BATCH_SUCCESS_BODY)) + + with SupermetricsClient(api_key="not-a-real-key", base_url=api_server.base_url) as client: + result = client.destinations.batch_update( + team_id=TEAM_ID, + destination_type="DWH_SNOWFLAKE", + updates=[ + BatchUpdateDestinationsBodyUpdatesItem(destination_id=8, new_secret="not-a-real-secret-1"), + BatchUpdateDestinationsBodyUpdatesItem(destination_id=9, new_secret="not-a-real-secret-2"), + ], + ) + + assert result.has_errors is False + assert len(result.results) == 2 + assert result.results[0].destination_id == 8 + assert result.results[0].status == "success" + + request = api_server.last_request + assert request.method == "PATCH" + assert request.path == BATCH + assert request.bearer_token == "not-a-real-key" + body = request.json() + assert body["type"] == "DWH_SNOWFLAKE" + assert len(body["updates"]) == 2 + assert body["updates"][0]["destination_id"] == 8 + assert body["updates"][0]["new_secret"] == "not-a-real-secret-1" + + def test_batch_update_partial_failure_surfaces_per_item_errors(self, api_server: MockAPIServer) -> None: + """A partial-failure batch returns has_errors=True with error details on failed items.""" + api_server.route(BATCH, ScriptedResponse(json_body=BATCH_PARTIAL_FAILURE_BODY)) + + with SupermetricsClient(api_key="not-a-real-key", base_url=api_server.base_url) as client: + result = client.destinations.batch_update( + team_id=TEAM_ID, + destination_type="DWH_SNOWFLAKE", + updates=[ + BatchUpdateDestinationsBodyUpdatesItem(destination_id=8, new_secret="not-a-real-secret-1"), + BatchUpdateDestinationsBodyUpdatesItem(destination_id=9, new_secret="not-a-real-secret-2"), + ], + ) + + assert result.has_errors is True + assert result.results[0].status == "success" + assert result.results[1].status == "error" + assert result.results[1].error_code == "INVALID_SECRET" + assert result.results[1].message == "Secret validation failed" + + def test_batch_update_validation_error_on_400(self, api_server: MockAPIServer) -> None: + """A 400 from the batch endpoint raises SupermetricsValidationError.""" + api_server.route( + BATCH, + ScriptedResponse(status=400, json_body=_error_envelope("INVALID_REQUEST", "Duplicate destination_id")), + ) + + with SupermetricsClient(api_key="not-a-real-key", base_url=api_server.base_url) as client: + with pytest.raises(SupermetricsValidationError) as exc_info: + client.destinations.batch_update(team_id=TEAM_ID, destination_type="DWH_SNOWFLAKE", updates=[]) + + assert exc_info.value.status_code == 400 + assert api_server.last_request.method == "PATCH" + class TestDestinationsAsyncResource: """Asynchronous destinations — same wire behaviour, own event hooks.""" @@ -649,6 +742,52 @@ async def test_get_usage_reports_an_unused_destination(self, api_server: MockAPI assert request.path == USAGE assert request.bearer_token == "not-a-real-key" + # ── batch_update (async) ──────────────────────────────────────────── + + @pytest.mark.asyncio + async def test_batch_update_sends_patch_and_unwraps_results(self, api_server: MockAPIServer) -> None: + """Async batch_update sends PATCH with type and updates, returns data.""" + api_server.route(BATCH, ScriptedResponse(json_body=BATCH_SUCCESS_BODY)) + + async with SupermetricsAsyncClient(api_key="not-a-real-key", base_url=api_server.base_url) as client: + result = await client.destinations.batch_update( + team_id=TEAM_ID, + destination_type="DWH_SNOWFLAKE", + updates=[ + BatchUpdateDestinationsBodyUpdatesItem(destination_id=8, new_secret="not-a-real-secret-1"), + BatchUpdateDestinationsBodyUpdatesItem(destination_id=9, new_secret="not-a-real-secret-2"), + ], + ) + + assert result.has_errors is False + assert len(result.results) == 2 + + request = api_server.last_request + assert request.method == "PATCH" + assert request.path == BATCH + body = request.json() + assert body["type"] == "DWH_SNOWFLAKE" + assert len(body["updates"]) == 2 + + @pytest.mark.asyncio + async def test_batch_update_partial_failure(self, api_server: MockAPIServer) -> None: + """Async partial-failure batch surfaces per-item errors.""" + api_server.route(BATCH, ScriptedResponse(json_body=BATCH_PARTIAL_FAILURE_BODY)) + + async with SupermetricsAsyncClient(api_key="not-a-real-key", base_url=api_server.base_url) as client: + result = await client.destinations.batch_update( + team_id=TEAM_ID, + destination_type="DWH_SNOWFLAKE", + updates=[ + BatchUpdateDestinationsBodyUpdatesItem(destination_id=8, new_secret="not-a-real-secret-1"), + BatchUpdateDestinationsBodyUpdatesItem(destination_id=9, new_secret="not-a-real-secret-2"), + ], + ) + + assert result.has_errors is True + assert result.results[1].status == "error" + assert result.results[1].error_code == "INVALID_SECRET" + class TestDestinationsRequestOptions: """Per-request overrides and the raw-response envelope, on destinations routes.""" diff --git a/tests/unit/test_destinations.py b/tests/unit/test_destinations.py index cde5339..21b33fa 100644 --- a/tests/unit/test_destinations.py +++ b/tests/unit/test_destinations.py @@ -8,6 +8,18 @@ import pytest from supermetrics._generated.supermetrics_api_client.client import Client as GeneratedClient +from supermetrics._generated.supermetrics_api_client.models.batch_update_destinations_body_updates_item import ( + BatchUpdateDestinationsBodyUpdatesItem as _BatchUpdateItem, +) +from supermetrics._generated.supermetrics_api_client.models.batch_update_destinations_response_200 import ( + BatchUpdateDestinationsResponse200 as _BatchResponse200, +) +from supermetrics._generated.supermetrics_api_client.models.batch_update_destinations_response_200_data import ( + BatchUpdateDestinationsResponse200Data as _BatchData, +) +from supermetrics._generated.supermetrics_api_client.models.batch_update_destinations_response_200_data_results_item import ( # noqa: E501 + BatchUpdateDestinationsResponse200DataResultsItem as _BatchResultItem, +) from supermetrics._generated.supermetrics_api_client.models.destination_info import DestinationInfo from supermetrics._generated.supermetrics_api_client.models.destination_list_item import DestinationListItem from supermetrics._generated.supermetrics_api_client.models.destination_list_response import DestinationListResponse @@ -1130,6 +1142,152 @@ def test_get_usage_api_error_on_500(self, destinations_resource: DestinationsRes finally: module.get_destination_usage.sync_detailed = original + # ── batch_update ──────────────────────────────────────────────────── + + def test_batch_update_success( + self, + destinations_resource: DestinationsResource, + meta: Meta, + ) -> None: + """Test successful batch update returns the data envelope.""" + import supermetrics.resources.destinations as module + + data = _BatchData(has_errors=False, results=[]) + original = module.batch_update_destinations.sync_detailed + module.batch_update_destinations.sync_detailed = MagicMock( + return_value=_make_success_response(_BatchResponse200(meta=meta, data=data)) + ) + + try: + result = destinations_resource.batch_update( + team_id=12345, + destination_type="DWH_SNOWFLAKE", + updates=[_BatchUpdateItem(destination_id=8, new_secret="new-pw-123")], + ) + + assert result.has_errors is False + assert result.results == [] + finally: + module.batch_update_destinations.sync_detailed = original + + def test_batch_update_partial_failure( + self, + destinations_resource: DestinationsResource, + meta: Meta, + ) -> None: + """Test batch update with partial failure reports has_errors and per-item results.""" + import supermetrics.resources.destinations as module + + results_items = [ + _BatchResultItem(destination_id=8, status="success"), + _BatchResultItem(destination_id=9, status="error", error_code="INVALID_SECRET", message="Secret too short"), + ] + data = _BatchData(has_errors=True, results=results_items) + original = module.batch_update_destinations.sync_detailed + module.batch_update_destinations.sync_detailed = MagicMock( + return_value=_make_success_response(_BatchResponse200(meta=meta, data=data)) + ) + + try: + result = destinations_resource.batch_update( + team_id=12345, + destination_type="DWH_SNOWFLAKE", + updates=[ + _BatchUpdateItem(destination_id=8, new_secret="new-pw-123"), + _BatchUpdateItem(destination_id=9, new_secret="x"), + ], + ) + + assert result.has_errors is True + assert len(result.results) == 2 + assert result.results[0].status == "success" + assert result.results[1].status == "error" + assert result.results[1].error_code == "INVALID_SECRET" + finally: + module.batch_update_destinations.sync_detailed = original + + def test_batch_update_passes_correct_params( + self, + destinations_resource: DestinationsResource, + meta: Meta, + ) -> None: + """Test that batch_update() forwards destination_type and updates to the generated client.""" + import supermetrics.resources.destinations as module + + data = _BatchData(has_errors=False, results=[]) + original = module.batch_update_destinations.sync_detailed + mock_sync = MagicMock(return_value=_make_success_response(_BatchResponse200(meta=meta, data=data))) + module.batch_update_destinations.sync_detailed = mock_sync + + try: + destinations_resource.batch_update( + team_id=12345, + destination_type="DWH_SNOWFLAKE", + updates=[_BatchUpdateItem(destination_id=8, new_secret="new-pw-123")], + ) + + call_kwargs = mock_sync.call_args.kwargs + assert call_kwargs["team_id"] == 12345 + body = call_kwargs["body"] + assert body.type_ == "DWH_SNOWFLAKE" + assert len(body.updates) == 1 + assert body.updates[0].destination_id == 8 + assert body.updates[0].new_secret == "new-pw-123" + finally: + module.batch_update_destinations.sync_detailed = original + + def test_batch_update_auth_error_on_401(self, destinations_resource: DestinationsResource) -> None: + """Test that batch_update() raises AuthenticationError on 401.""" + import supermetrics.resources.destinations as module + + original = module.batch_update_destinations.sync_detailed + module.batch_update_destinations.sync_detailed = MagicMock( + return_value=_make_error_response(HTTPStatus.UNAUTHORIZED, "UNAUTHORIZED", "Invalid API key") + ) + + try: + with pytest.raises(AuthenticationError) as exc_info: + destinations_resource.batch_update(team_id=12345, destination_type="DWH_SNOWFLAKE", updates=[]) + + assert exc_info.value.status_code == 401 + finally: + module.batch_update_destinations.sync_detailed = original + + def test_batch_update_validation_error_on_400(self, destinations_resource: DestinationsResource) -> None: + """Test that batch_update() raises ValidationError on 400.""" + import supermetrics.resources.destinations as module + from supermetrics.exceptions import SupermetricsValidationError + + original = module.batch_update_destinations.sync_detailed + module.batch_update_destinations.sync_detailed = MagicMock( + return_value=_make_error_response(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", "Duplicate destination_id") + ) + + try: + with pytest.raises(SupermetricsValidationError) as exc_info: + destinations_resource.batch_update(team_id=12345, destination_type="DWH_SNOWFLAKE", updates=[]) + + assert exc_info.value.status_code == 400 + finally: + module.batch_update_destinations.sync_detailed = original + + def test_batch_update_api_error_on_500(self, destinations_resource: DestinationsResource) -> None: + """Test that batch_update() raises APIError on 500.""" + import supermetrics.resources.destinations as module + + original = module.batch_update_destinations.sync_detailed + module.batch_update_destinations.sync_detailed = MagicMock( + return_value=_make_error_response(HTTPStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "Server error") + ) + + try: + with pytest.raises(APIError) as exc_info: + destinations_resource.batch_update(team_id=12345, destination_type="DWH_SNOWFLAKE", updates=[]) + + assert exc_info.value.status_code == 500 + finally: + module.batch_update_destinations.sync_detailed = original + class TestDestinationsAsyncResource: """Test suite for DestinationsAsyncResource (asynchronous).""" @@ -2220,3 +2378,155 @@ async def test_get_usage_api_error_on_500(self, destinations_resource: Destinati assert exc_info.value.status_code == 500 finally: module.get_destination_usage.asyncio_detailed = original + + # ── batch_update (async) ──────────────────────────────────────────── + + @pytest.mark.asyncio + async def test_batch_update_success( + self, + destinations_resource: DestinationsAsyncResource, + meta: Meta, + ) -> None: + """Test async batch update returns the data envelope.""" + import supermetrics.resources.destinations as module + + data = _BatchData(has_errors=False, results=[]) + original = module.batch_update_destinations.asyncio_detailed + module.batch_update_destinations.asyncio_detailed = AsyncMock( + return_value=_make_success_response(_BatchResponse200(meta=meta, data=data)) + ) + + try: + result = await destinations_resource.batch_update( + team_id=12345, + destination_type="DWH_SNOWFLAKE", + updates=[_BatchUpdateItem(destination_id=8, new_secret="new-pw-123")], + ) + + assert result.has_errors is False + assert result.results == [] + finally: + module.batch_update_destinations.asyncio_detailed = original + + @pytest.mark.asyncio + async def test_batch_update_partial_failure( + self, + destinations_resource: DestinationsAsyncResource, + meta: Meta, + ) -> None: + """Test async batch update with partial failure.""" + import supermetrics.resources.destinations as module + + results_items = [ + _BatchResultItem(destination_id=8, status="success"), + _BatchResultItem(destination_id=9, status="error", error_code="INVALID_SECRET", message="Secret too short"), + ] + data = _BatchData(has_errors=True, results=results_items) + original = module.batch_update_destinations.asyncio_detailed + module.batch_update_destinations.asyncio_detailed = AsyncMock( + return_value=_make_success_response(_BatchResponse200(meta=meta, data=data)) + ) + + try: + result = await destinations_resource.batch_update( + team_id=12345, + destination_type="DWH_SNOWFLAKE", + updates=[ + _BatchUpdateItem(destination_id=8, new_secret="new-pw-123"), + _BatchUpdateItem(destination_id=9, new_secret="x"), + ], + ) + + assert result.has_errors is True + assert len(result.results) == 2 + assert result.results[0].status == "success" + assert result.results[1].status == "error" + assert result.results[1].error_code == "INVALID_SECRET" + finally: + module.batch_update_destinations.asyncio_detailed = original + + @pytest.mark.asyncio + async def test_batch_update_passes_correct_params( + self, + destinations_resource: DestinationsAsyncResource, + meta: Meta, + ) -> None: + """Test async batch_update() forwards destination_type and updates.""" + import supermetrics.resources.destinations as module + + data = _BatchData(has_errors=False, results=[]) + original = module.batch_update_destinations.asyncio_detailed + mock_async = AsyncMock(return_value=_make_success_response(_BatchResponse200(meta=meta, data=data))) + module.batch_update_destinations.asyncio_detailed = mock_async + + try: + await destinations_resource.batch_update( + team_id=12345, + destination_type="DWH_SNOWFLAKE", + updates=[_BatchUpdateItem(destination_id=8, new_secret="new-pw-123")], + ) + + call_kwargs = mock_async.call_args.kwargs + assert call_kwargs["team_id"] == 12345 + body = call_kwargs["body"] + assert body.type_ == "DWH_SNOWFLAKE" + assert len(body.updates) == 1 + assert body.updates[0].destination_id == 8 + assert body.updates[0].new_secret == "new-pw-123" + finally: + module.batch_update_destinations.asyncio_detailed = original + + @pytest.mark.asyncio + async def test_batch_update_auth_error_on_401(self, destinations_resource: DestinationsAsyncResource) -> None: + """Test async batch_update() raises AuthenticationError on 401.""" + import supermetrics.resources.destinations as module + + original = module.batch_update_destinations.asyncio_detailed + module.batch_update_destinations.asyncio_detailed = AsyncMock( + return_value=_make_error_response(HTTPStatus.UNAUTHORIZED, "UNAUTHORIZED", "Invalid API key") + ) + + try: + with pytest.raises(AuthenticationError) as exc_info: + await destinations_resource.batch_update(team_id=12345, destination_type="DWH_SNOWFLAKE", updates=[]) + + assert exc_info.value.status_code == 401 + finally: + module.batch_update_destinations.asyncio_detailed = original + + @pytest.mark.asyncio + async def test_batch_update_validation_error_on_400(self, destinations_resource: DestinationsAsyncResource) -> None: + """Test async batch_update() raises ValidationError on 400.""" + import supermetrics.resources.destinations as module + from supermetrics.exceptions import SupermetricsValidationError + + original = module.batch_update_destinations.asyncio_detailed + module.batch_update_destinations.asyncio_detailed = AsyncMock( + return_value=_make_error_response(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", "Duplicate destination_id") + ) + + try: + with pytest.raises(SupermetricsValidationError) as exc_info: + await destinations_resource.batch_update(team_id=12345, destination_type="DWH_SNOWFLAKE", updates=[]) + + assert exc_info.value.status_code == 400 + finally: + module.batch_update_destinations.asyncio_detailed = original + + @pytest.mark.asyncio + async def test_batch_update_api_error_on_500(self, destinations_resource: DestinationsAsyncResource) -> None: + """Test async batch_update() raises APIError on 500.""" + import supermetrics.resources.destinations as module + + original = module.batch_update_destinations.asyncio_detailed + module.batch_update_destinations.asyncio_detailed = AsyncMock( + return_value=_make_error_response(HTTPStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "Server error") + ) + + try: + with pytest.raises(APIError) as exc_info: + await destinations_resource.batch_update(team_id=12345, destination_type="DWH_SNOWFLAKE", updates=[]) + + assert exc_info.value.status_code == 500 + finally: + module.batch_update_destinations.asyncio_detailed = original