From e2c0ea1cf239a5f15627649aa60189d36c023e7c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:14:56 +0000 Subject: [PATCH 01/10] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 02a2d07..da68dcd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-10329cc51f6b17a164e474568823469dcf6dda933ed001cf5da70440a7e55dd7.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-72dfa6ef89d0b2527117840e68f775c658befedc3d50b48851ec6e84b004224a.yml openapi_spec_hash: 1c0790919f1143b6ec46ba0c811a86f4 -config_hash: 18c6c7df17bad3320b9f2c126947f00a +config_hash: 55dcf0eada48b50757a41e158645a7f8 From 7e3b0bdd3b2daef10d8f6f419ae70ff1cd88b615 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:47:29 +0000 Subject: [PATCH 02/10] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index da68dcd..f7a37d8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-72dfa6ef89d0b2527117840e68f775c658befedc3d50b48851ec6e84b004224a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-3b6b2988e81f1a911264033c615fc06f8c9205c880539fea0f5cce25bfd80a0b.yml openapi_spec_hash: 1c0790919f1143b6ec46ba0c811a86f4 -config_hash: 55dcf0eada48b50757a41e158645a7f8 +config_hash: b1872b239ae9e5b5e3d71167a4241fae From 731c060413e39daa1f74ba6e3c46a687d7b2c67f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 09:59:41 +0000 Subject: [PATCH 03/10] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index f7a37d8..da68dcd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-3b6b2988e81f1a911264033c615fc06f8c9205c880539fea0f5cce25bfd80a0b.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-72dfa6ef89d0b2527117840e68f775c658befedc3d50b48851ec6e84b004224a.yml openapi_spec_hash: 1c0790919f1143b6ec46ba0c811a86f4 -config_hash: b1872b239ae9e5b5e3d71167a4241fae +config_hash: 55dcf0eada48b50757a41e158645a7f8 From ed19bab778dbe4c5dc148b9d1c4ee22ffde38308 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:45:49 +0000 Subject: [PATCH 04/10] fix: regenerate SDK for PartnerMAX contract 1.5.3 --- .stats.yml | 8 +- README.md | 141 +++-- api.md | 11 +- src/partnermax/_client.py | 24 +- src/partnermax/_files.py | 2 +- src/partnermax/pagination.py | 68 +++ src/partnermax/resources/dealers/dealers.py | 260 +++------ .../resources/dealers/nlt/offers.py | 63 +-- .../resources/dealers/nlt_settings.py | 42 +- .../resources/dealers/vehicles/images.py | 31 +- .../resources/dealers/vehicles/vehicles.py | 123 ++-- src/partnermax/resources/keys.py | 36 +- src/partnermax/types/__init__.py | 2 - src/partnermax/types/dealer_create_params.py | 36 -- src/partnermax/types/dealer_list_response.py | 18 - .../types/dealers/bulk_row_outcome.py | 16 +- .../types/dealers/down_payment_tiers.py | 62 +-- .../types/dealers/down_payment_tiers_param.py | 62 +-- src/partnermax/types/dealers/nlt/__init__.py | 1 - .../types/dealers/nlt/nlt_offer_summary.py | 10 +- .../types/dealers/nlt/offer_list_response.py | 18 - .../dealers/nlt/offer_retrieve_response.py | 93 ++-- .../dealers/nlt_setting_update_params.py | 4 +- src/partnermax/types/dealers/nlt_settings.py | 8 +- .../types/dealers/vehicle_bulk_params.py | 27 +- .../types/dealers/vehicle_create_params.py | 21 +- .../types/dealers/vehicle_detail.py | 27 +- .../types/dealers/vehicle_summary.py | 5 +- .../dealers/vehicles/image_create_params.py | 4 +- .../api_resources/dealers/nlt/test_offers.py | 19 +- tests/api_resources/dealers/test_vehicles.py | 19 +- .../dealers/vehicles/test_images.py | 20 +- tests/api_resources/test_dealers.py | 524 +++++++----------- tests/test_client.py | 52 +- 34 files changed, 786 insertions(+), 1071 deletions(-) create mode 100644 src/partnermax/pagination.py delete mode 100644 src/partnermax/types/dealer_create_params.py delete mode 100644 src/partnermax/types/dealer_list_response.py delete mode 100644 src/partnermax/types/dealers/nlt/offer_list_response.py diff --git a/.stats.yml b/.stats.yml index da68dcd..b84d635 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-72dfa6ef89d0b2527117840e68f775c658befedc3d50b48851ec6e84b004224a.yml -openapi_spec_hash: 1c0790919f1143b6ec46ba0c811a86f4 -config_hash: 55dcf0eada48b50757a41e158645a7f8 +configured_endpoints: 20 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-12484954a328c4cc183eb761854de199b41e2b40ffcfb788217db027941820d1.yml +openapi_spec_hash: e9a8ecc3b1b9bffadec88bab183338a4 +config_hash: 410d10ad3652f4fdf97d2bd563de0d64 diff --git a/README.md b/README.md index 13465f4..4051510 100644 --- a/README.md +++ b/README.md @@ -34,18 +34,11 @@ client = Partnermax( environment="sandbox", ) -dealer_detail = client.dealers.create( - address="xx", - business_name="Rossi Automobili S.R.L.", - city="xx", - contact_email="info@rossi-auto.it", - contact_phone="xxxxx", - postal_code="20121", - primary_domain="rossi-auto.it", - province_code="MI", - vat_number="IT01234567890", +page = client.dealers.list( + limit=10, + status="active", ) -print(dealer_detail.dealer_id) +print(page.data) ``` While you can provide an `api_key` keyword argument, @@ -70,18 +63,11 @@ client = AsyncPartnermax( async def main() -> None: - dealer_detail = await client.dealers.create( - address="xx", - business_name="Rossi Automobili S.R.L.", - city="xx", - contact_email="info@rossi-auto.it", - contact_phone="xxxxx", - postal_code="20121", - primary_domain="rossi-auto.it", - province_code="MI", - vat_number="IT01234567890", + page = await client.dealers.list( + limit=10, + status="active", ) - print(dealer_detail.dealer_id) + print(page.data) asyncio.run(main()) @@ -114,18 +100,11 @@ async def main() -> None: api_key=os.environ.get("PARTNERMAX_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: - dealer_detail = await client.dealers.create( - address="xx", - business_name="Rossi Automobili S.R.L.", - city="xx", - contact_email="info@rossi-auto.it", - contact_phone="xxxxx", - postal_code="20121", - primary_domain="rossi-auto.it", - province_code="MI", - vat_number="IT01234567890", + page = await client.dealers.list( + limit=10, + status="active", ) - print(dealer_detail.dealer_id) + print(page.data) asyncio.run(main()) @@ -140,6 +119,81 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## Pagination + +List methods in the Partnermax API are paginated. + +This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: + +```python +from partnermax import Partnermax + +client = Partnermax() + +all_dealers = [] +# Automatically fetches more pages as needed. +for dealer in client.dealers.list( + limit=10, + status="active", +): + # Do something with dealer here + all_dealers.append(dealer) +print(all_dealers) +``` + +Or, asynchronously: + +```python +import asyncio +from partnermax import AsyncPartnermax + +client = AsyncPartnermax() + + +async def main() -> None: + all_dealers = [] + # Iterate through items across all pages, issuing requests as needed. + async for dealer in client.dealers.list( + limit=10, + status="active", + ): + all_dealers.append(dealer) + print(all_dealers) + + +asyncio.run(main()) +``` + +Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: + +```python +first_page = await client.dealers.list( + limit=10, + status="active", +) +if first_page.has_next_page(): + print(f"will fetch next page using these details: {first_page.next_page_info()}") + next_page = await first_page.get_next_page() + print(f"number of items we just fetched: {len(next_page.data)}") + +# Remove `await` for non-async usage. +``` + +Or just work directly with the returned data: + +```python +first_page = await client.dealers.list( + limit=10, + status="active", +) + +print(f"next page cursor: {first_page.next_cursor}") # => "next page cursor: ..." +for dealer in first_page.data: + print(dealer.dealer_id) + +# Remove `await` for non-async usage. +``` + ## Nested params Nested parameters are dictionaries, typed using `TypedDict`, for example: @@ -170,6 +224,25 @@ nlt_settings = client.dealers.nlt_settings.update( print(nlt_settings.down_payment_tiers) ``` +## File uploads + +Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. + +```python +from pathlib import Path +from partnermax import Partnermax + +client = Partnermax() + +client.dealers.vehicles.images.create( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + file=Path("/path/to/file"), +) +``` + +The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `partnermax.APIConnectionError` is raised. @@ -297,7 +370,7 @@ response = client.dealers.with_raw_response.list() print(response.headers.get('X-My-Header')) dealer = response.parse() # get the object that `dealers.list()` would have returned -print(dealer.data) +print(dealer.dealer_id) ``` These methods return an [`APIResponse`](https://github.com/DealerMax-app/partnermax-python/tree/main/src/partnermax/_response.py) object. diff --git a/api.md b/api.md index 1b965c3..301ed11 100644 --- a/api.md +++ b/api.md @@ -17,15 +17,14 @@ Methods: Types: ```python -from partnermax.types import DealerDetail, DealerSummary, DealerListResponse +from partnermax.types import DealerDetail, DealerSummary ``` Methods: -- client.dealers.create(\*\*params) -> DealerDetail - client.dealers.retrieve(dealer_id) -> DealerDetail - client.dealers.update(dealer_id, \*\*params) -> DealerDetail -- client.dealers.list(\*\*params) -> DealerListResponse +- client.dealers.list(\*\*params) -> SyncCursorPage[DealerSummary] - client.dealers.delete(dealer_id) -> None ## NltSettings @@ -48,13 +47,13 @@ Methods: Types: ```python -from partnermax.types.dealers.nlt import NltOfferSummary, OfferRetrieveResponse, OfferListResponse +from partnermax.types.dealers.nlt import NltOfferSummary, OfferRetrieveResponse ``` Methods: - client.dealers.nlt.offers.retrieve(offer_id, \*, dealer_id) -> OfferRetrieveResponse -- client.dealers.nlt.offers.list(dealer_id, \*\*params) -> OfferListResponse +- client.dealers.nlt.offers.list(dealer_id, \*\*params) -> SyncCursorPage[NltOfferSummary] ## Vehicles @@ -76,7 +75,7 @@ Methods: - client.dealers.vehicles.create(dealer_id, \*\*params) -> VehicleDetail - client.dealers.vehicles.retrieve(vehicle_id, \*, dealer_id, \*\*params) -> VehicleDetail - client.dealers.vehicles.update(vehicle_id, \*, dealer_id, \*\*params) -> VehicleDetail -- client.dealers.vehicles.list(dealer_id, \*\*params) -> VehicleList +- client.dealers.vehicles.list(dealer_id, \*\*params) -> SyncCursorPage[VehicleSummary] - client.dealers.vehicles.delete(vehicle_id, \*, dealer_id) -> None - client.dealers.vehicles.bulk(dealer_id, \*\*params) -> BulkCreateVehiclesResponse diff --git a/src/partnermax/_client.py b/src/partnermax/_client.py index dc3e234..b4a7950 100644 --- a/src/partnermax/_client.py +++ b/src/partnermax/_client.py @@ -153,9 +153,9 @@ def __init__( @cached_property def keys(self) -> KeysResource: - """API key lifecycle management — issue, list, revoke. + """API key lifecycle management. - The partner authenticates every request with `X-Api-Key` (preferred) or `Authorization: Bearer `; the server identifies the partner from the key and scopes all reads/writes to dealers owned by that partner. + PartnerMAX v1 allows one active API key per partner/environment; partners authenticate every request with `X-Api-Key` (preferred) or `Authorization: Bearer `, and replacement is handled through DealerMAX support. """ from .resources.keys import KeysResource @@ -410,9 +410,9 @@ def __init__( @cached_property def keys(self) -> AsyncKeysResource: - """API key lifecycle management — issue, list, revoke. + """API key lifecycle management. - The partner authenticates every request with `X-Api-Key` (preferred) or `Authorization: Bearer `; the server identifies the partner from the key and scopes all reads/writes to dealers owned by that partner. + PartnerMAX v1 allows one active API key per partner/environment; partners authenticate every request with `X-Api-Key` (preferred) or `Authorization: Bearer `, and replacement is handled through DealerMAX support. """ from .resources.keys import AsyncKeysResource @@ -581,9 +581,9 @@ def __init__(self, client: Partnermax) -> None: @cached_property def keys(self) -> keys.KeysResourceWithRawResponse: - """API key lifecycle management — issue, list, revoke. + """API key lifecycle management. - The partner authenticates every request with `X-Api-Key` (preferred) or `Authorization: Bearer `; the server identifies the partner from the key and scopes all reads/writes to dealers owned by that partner. + PartnerMAX v1 allows one active API key per partner/environment; partners authenticate every request with `X-Api-Key` (preferred) or `Authorization: Bearer `, and replacement is handled through DealerMAX support. """ from .resources.keys import KeysResourceWithRawResponse @@ -605,9 +605,9 @@ def __init__(self, client: AsyncPartnermax) -> None: @cached_property def keys(self) -> keys.AsyncKeysResourceWithRawResponse: - """API key lifecycle management — issue, list, revoke. + """API key lifecycle management. - The partner authenticates every request with `X-Api-Key` (preferred) or `Authorization: Bearer `; the server identifies the partner from the key and scopes all reads/writes to dealers owned by that partner. + PartnerMAX v1 allows one active API key per partner/environment; partners authenticate every request with `X-Api-Key` (preferred) or `Authorization: Bearer `, and replacement is handled through DealerMAX support. """ from .resources.keys import AsyncKeysResourceWithRawResponse @@ -629,9 +629,9 @@ def __init__(self, client: Partnermax) -> None: @cached_property def keys(self) -> keys.KeysResourceWithStreamingResponse: - """API key lifecycle management — issue, list, revoke. + """API key lifecycle management. - The partner authenticates every request with `X-Api-Key` (preferred) or `Authorization: Bearer `; the server identifies the partner from the key and scopes all reads/writes to dealers owned by that partner. + PartnerMAX v1 allows one active API key per partner/environment; partners authenticate every request with `X-Api-Key` (preferred) or `Authorization: Bearer `, and replacement is handled through DealerMAX support. """ from .resources.keys import KeysResourceWithStreamingResponse @@ -653,9 +653,9 @@ def __init__(self, client: AsyncPartnermax) -> None: @cached_property def keys(self) -> keys.AsyncKeysResourceWithStreamingResponse: - """API key lifecycle management — issue, list, revoke. + """API key lifecycle management. - The partner authenticates every request with `X-Api-Key` (preferred) or `Authorization: Bearer `; the server identifies the partner from the key and scopes all reads/writes to dealers owned by that partner. + PartnerMAX v1 allows one active API key per partner/environment; partners authenticate every request with `X-Api-Key` (preferred) or `Authorization: Bearer `, and replacement is handled through DealerMAX support. """ from .resources.keys import AsyncKeysResourceWithStreamingResponse diff --git a/src/partnermax/_files.py b/src/partnermax/_files.py index 76da9e0..d962650 100644 --- a/src/partnermax/_files.py +++ b/src/partnermax/_files.py @@ -36,7 +36,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: if not is_file_content(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/DealerMax-app/partnermax-python/tree/main#file-uploads" ) from None diff --git a/src/partnermax/pagination.py b/src/partnermax/pagination.py new file mode 100644 index 0000000..8192422 --- /dev/null +++ b/src/partnermax/pagination.py @@ -0,0 +1,68 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Generic, TypeVar, Optional +from typing_extensions import override + +from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage + +__all__ = ["SyncCursorPage", "AsyncCursorPage"] + +_T = TypeVar("_T") + + +class SyncCursorPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + data: List[_T] + has_more: Optional[bool] = None + next_cursor: Optional[str] = None + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def has_next_page(self) -> bool: + has_more = self.has_more + if has_more is not None and has_more is False: + return False + + return super().has_next_page() + + @override + def next_page_info(self) -> Optional[PageInfo]: + next_cursor = self.next_cursor + if not next_cursor: + return None + + return PageInfo(params={"cursor": next_cursor}) + + +class AsyncCursorPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + data: List[_T] + has_more: Optional[bool] = None + next_cursor: Optional[str] = None + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def has_next_page(self) -> bool: + has_more = self.has_more + if has_more is not None and has_more is False: + return False + + return super().has_next_page() + + @override + def next_page_info(self) -> Optional[PageInfo]: + next_cursor = self.next_cursor + if not next_cursor: + return None + + return PageInfo(params={"cursor": next_cursor}) diff --git a/src/partnermax/resources/dealers/dealers.py b/src/partnermax/resources/dealers/dealers.py index 3df5d55..188d588 100644 --- a/src/partnermax/resources/dealers/dealers.py +++ b/src/partnermax/resources/dealers/dealers.py @@ -2,12 +2,13 @@ from __future__ import annotations +import typing_extensions from typing import Dict, Optional from typing_extensions import Literal import httpx -from ...types import dealer_list_params, dealer_create_params, dealer_update_params +from ...types import dealer_list_params, dealer_update_params from .nlt.nlt import ( NltResource, AsyncNltResource, @@ -26,6 +27,7 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from ...pagination import SyncCursorPage, AsyncCursorPage from .nlt_settings import ( NltSettingsResource, AsyncNltSettingsResource, @@ -34,7 +36,7 @@ NltSettingsResourceWithStreamingResponse, AsyncNltSettingsResourceWithStreamingResponse, ) -from ..._base_client import make_request_options +from ..._base_client import AsyncPaginator, make_request_options from .vehicles.vehicles import ( VehiclesResource, AsyncVehiclesResource, @@ -44,7 +46,7 @@ AsyncVehiclesResourceWithStreamingResponse, ) from ...types.dealer_detail import DealerDetail -from ...types.dealer_list_response import DealerListResponse +from ...types.dealer_summary import DealerSummary __all__ = ["DealersResource", "AsyncDealersResource"] @@ -87,65 +89,7 @@ def with_streaming_response(self) -> DealersResourceWithStreamingResponse: """ return DealersResourceWithStreamingResponse(self) - def create( - self, - *, - address: str, - business_name: str, - city: str, - contact_email: str, - contact_phone: str, - postal_code: str, - primary_domain: str, - province_code: str, - vat_number: str, - activate: bool | Omit = omit, - metadata: Dict[str, str] | Omit = omit, - idempotency_key: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DealerDetail: - """ - Provision a new dealer as child of the calling partner. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} - return self._post( - "/v1/dealers", - body=maybe_transform( - { - "address": address, - "business_name": business_name, - "city": city, - "contact_email": contact_email, - "contact_phone": contact_phone, - "postal_code": postal_code, - "primary_domain": primary_domain, - "province_code": province_code, - "vat_number": vat_number, - "activate": activate, - "metadata": metadata, - }, - dealer_create_params.DealerCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DealerDetail, - ) - + @typing_extensions.deprecated("deprecated") def retrieve( self, dealer_id: str, @@ -180,6 +124,7 @@ def retrieve( cast_to=DealerDetail, ) + @typing_extensions.deprecated("deprecated") def update( self, dealer_id: str, @@ -239,6 +184,7 @@ def update( cast_to=DealerDetail, ) + @typing_extensions.deprecated("deprecated") def list( self, *, @@ -251,7 +197,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DealerListResponse: + ) -> SyncCursorPage[DealerSummary]: """List dealers owned by the calling partner. Cursor-paginated. @@ -265,8 +211,9 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - return self._get( + return self._get_api_list( "/v1/dealers", + page=SyncCursorPage[DealerSummary], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -281,9 +228,10 @@ def list( dealer_list_params.DealerListParams, ), ), - cast_to=DealerListResponse, + model=DealerSummary, ) + @typing_extensions.deprecated("deprecated") def delete( self, dealer_id: str, @@ -358,65 +306,7 @@ def with_streaming_response(self) -> AsyncDealersResourceWithStreamingResponse: """ return AsyncDealersResourceWithStreamingResponse(self) - async def create( - self, - *, - address: str, - business_name: str, - city: str, - contact_email: str, - contact_phone: str, - postal_code: str, - primary_domain: str, - province_code: str, - vat_number: str, - activate: bool | Omit = omit, - metadata: Dict[str, str] | Omit = omit, - idempotency_key: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DealerDetail: - """ - Provision a new dealer as child of the calling partner. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} - return await self._post( - "/v1/dealers", - body=await async_maybe_transform( - { - "address": address, - "business_name": business_name, - "city": city, - "contact_email": contact_email, - "contact_phone": contact_phone, - "postal_code": postal_code, - "primary_domain": primary_domain, - "province_code": province_code, - "vat_number": vat_number, - "activate": activate, - "metadata": metadata, - }, - dealer_create_params.DealerCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DealerDetail, - ) - + @typing_extensions.deprecated("deprecated") async def retrieve( self, dealer_id: str, @@ -451,6 +341,7 @@ async def retrieve( cast_to=DealerDetail, ) + @typing_extensions.deprecated("deprecated") async def update( self, dealer_id: str, @@ -510,7 +401,8 @@ async def update( cast_to=DealerDetail, ) - async def list( + @typing_extensions.deprecated("deprecated") + def list( self, *, cursor: Optional[str] | Omit = omit, @@ -522,7 +414,7 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DealerListResponse: + ) -> AsyncPaginator[DealerSummary, AsyncCursorPage[DealerSummary]]: """List dealers owned by the calling partner. Cursor-paginated. @@ -536,14 +428,15 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._get( + return self._get_api_list( "/v1/dealers", + page=AsyncCursorPage[DealerSummary], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform( + query=maybe_transform( { "cursor": cursor, "limit": limit, @@ -552,9 +445,10 @@ async def list( dealer_list_params.DealerListParams, ), ), - cast_to=DealerListResponse, + model=DealerSummary, ) + @typing_extensions.deprecated("deprecated") async def delete( self, dealer_id: str, @@ -595,20 +489,25 @@ class DealersResourceWithRawResponse: def __init__(self, dealers: DealersResource) -> None: self._dealers = dealers - self.create = to_raw_response_wrapper( - dealers.create, - ) - self.retrieve = to_raw_response_wrapper( - dealers.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + dealers.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.update = to_raw_response_wrapper( - dealers.update, + self.update = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + dealers.update, # pyright: ignore[reportDeprecated], + ) ) - self.list = to_raw_response_wrapper( - dealers.list, + self.list = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + dealers.list, # pyright: ignore[reportDeprecated], + ) ) - self.delete = to_raw_response_wrapper( - dealers.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + dealers.delete, # pyright: ignore[reportDeprecated], + ) ) @cached_property @@ -632,20 +531,25 @@ class AsyncDealersResourceWithRawResponse: def __init__(self, dealers: AsyncDealersResource) -> None: self._dealers = dealers - self.create = async_to_raw_response_wrapper( - dealers.create, - ) - self.retrieve = async_to_raw_response_wrapper( - dealers.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + dealers.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.update = async_to_raw_response_wrapper( - dealers.update, + self.update = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + dealers.update, # pyright: ignore[reportDeprecated], + ) ) - self.list = async_to_raw_response_wrapper( - dealers.list, + self.list = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + dealers.list, # pyright: ignore[reportDeprecated], + ) ) - self.delete = async_to_raw_response_wrapper( - dealers.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + dealers.delete, # pyright: ignore[reportDeprecated], + ) ) @cached_property @@ -669,20 +573,25 @@ class DealersResourceWithStreamingResponse: def __init__(self, dealers: DealersResource) -> None: self._dealers = dealers - self.create = to_streamed_response_wrapper( - dealers.create, + self.retrieve = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + dealers.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.retrieve = to_streamed_response_wrapper( - dealers.retrieve, + self.update = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + dealers.update, # pyright: ignore[reportDeprecated], + ) ) - self.update = to_streamed_response_wrapper( - dealers.update, + self.list = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + dealers.list, # pyright: ignore[reportDeprecated], + ) ) - self.list = to_streamed_response_wrapper( - dealers.list, - ) - self.delete = to_streamed_response_wrapper( - dealers.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + dealers.delete, # pyright: ignore[reportDeprecated], + ) ) @cached_property @@ -706,20 +615,25 @@ class AsyncDealersResourceWithStreamingResponse: def __init__(self, dealers: AsyncDealersResource) -> None: self._dealers = dealers - self.create = async_to_streamed_response_wrapper( - dealers.create, - ) - self.retrieve = async_to_streamed_response_wrapper( - dealers.retrieve, + self.retrieve = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + dealers.retrieve, # pyright: ignore[reportDeprecated], + ) ) - self.update = async_to_streamed_response_wrapper( - dealers.update, + self.update = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + dealers.update, # pyright: ignore[reportDeprecated], + ) ) - self.list = async_to_streamed_response_wrapper( - dealers.list, + self.list = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + dealers.list, # pyright: ignore[reportDeprecated], + ) ) - self.delete = async_to_streamed_response_wrapper( - dealers.delete, + self.delete = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + dealers.delete, # pyright: ignore[reportDeprecated], + ) ) @cached_property diff --git a/src/partnermax/resources/dealers/nlt/offers.py b/src/partnermax/resources/dealers/nlt/offers.py index b314d46..e488a77 100644 --- a/src/partnermax/resources/dealers/nlt/offers.py +++ b/src/partnermax/resources/dealers/nlt/offers.py @@ -7,7 +7,7 @@ import httpx from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._utils import path_template, maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -16,9 +16,10 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ...._base_client import make_request_options +from ....pagination import SyncCursorPage, AsyncCursorPage +from ...._base_client import AsyncPaginator, make_request_options from ....types.dealers.nlt import offer_list_params -from ....types.dealers.nlt.offer_list_response import OfferListResponse +from ....types.dealers.nlt.nlt_offer_summary import NltOfferSummary from ....types.dealers.nlt.offer_retrieve_response import OfferRetrieveResponse __all__ = ["OffersResource", "AsyncOffersResource"] @@ -101,21 +102,14 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> OfferListResponse: - """Listing of NLT offers with monthly canon repriced for this dealer. - - Strategy: - - 1. + ) -> SyncCursorPage[NltOfferSummary]: + """ + Listing of NLT offers with monthly canon repriced for this dealer. - Resolve + ACL the dealer. - 2. Pull at most `limit + 1` offers from the catalog after the cursor. The extra - row lets us know if there's a next page without a second COUNT(\\**) query. - 3. Apply text/enum filters server-side via SQL where possible (brand, segment, - fuel) and the numeric `canone_max_eur` filter in Python after the pricing - pass (the DB has no "displayed canon" column; we synthesize it per dealer). - 4. For each surviving offer, price the (duration, km) cells the caller filtered - to (if specified) or all 18, pick the cheapest cell as the headline. + The response is cursor-paginated and dealer-aware: filters are applied to the + shared NLT catalogue, then each returned offer is repriced with the dealer's + configured mark-up, down-payment tiers, duration, and yearly-km filters. The + headline canon is the cheapest eligible priced cell. Args: vehicle_type: Macro discriminator: 'auto' (passenger vehicles) or 'vcom' (light commercial ≤35 @@ -132,8 +126,9 @@ def list( """ if not dealer_id: raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") - return self._get( + return self._get_api_list( path_template("/v1/dealers/{dealer_id}/nlt/offers", dealer_id=dealer_id), + page=SyncCursorPage[NltOfferSummary], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -154,7 +149,7 @@ def list( offer_list_params.OfferListParams, ), ), - cast_to=OfferListResponse, + model=NltOfferSummary, ) @@ -216,7 +211,7 @@ async def retrieve( cast_to=OfferRetrieveResponse, ) - async def list( + def list( self, dealer_id: str, *, @@ -235,21 +230,14 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> OfferListResponse: - """Listing of NLT offers with monthly canon repriced for this dealer. - - Strategy: - - 1. + ) -> AsyncPaginator[NltOfferSummary, AsyncCursorPage[NltOfferSummary]]: + """ + Listing of NLT offers with monthly canon repriced for this dealer. - Resolve + ACL the dealer. - 2. Pull at most `limit + 1` offers from the catalog after the cursor. The extra - row lets us know if there's a next page without a second COUNT(\\**) query. - 3. Apply text/enum filters server-side via SQL where possible (brand, segment, - fuel) and the numeric `canone_max_eur` filter in Python after the pricing - pass (the DB has no "displayed canon" column; we synthesize it per dealer). - 4. For each surviving offer, price the (duration, km) cells the caller filtered - to (if specified) or all 18, pick the cheapest cell as the headline. + The response is cursor-paginated and dealer-aware: filters are applied to the + shared NLT catalogue, then each returned offer is repriced with the dealer's + configured mark-up, down-payment tiers, duration, and yearly-km filters. The + headline canon is the cheapest eligible priced cell. Args: vehicle_type: Macro discriminator: 'auto' (passenger vehicles) or 'vcom' (light commercial ≤35 @@ -266,14 +254,15 @@ async def list( """ if not dealer_id: raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") - return await self._get( + return self._get_api_list( path_template("/v1/dealers/{dealer_id}/nlt/offers", dealer_id=dealer_id), + page=AsyncCursorPage[NltOfferSummary], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform( + query=maybe_transform( { "brand": brand, "canone_max_eur": canone_max_eur, @@ -288,7 +277,7 @@ async def list( offer_list_params.OfferListParams, ), ), - cast_to=OfferListResponse, + model=NltOfferSummary, ) diff --git a/src/partnermax/resources/dealers/nlt_settings.py b/src/partnermax/resources/dealers/nlt_settings.py index ecc2855..29f7c78 100644 --- a/src/partnermax/resources/dealers/nlt_settings.py +++ b/src/partnermax/resources/dealers/nlt_settings.py @@ -97,7 +97,7 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> NltSettings: """ - Set markup percent (0-10) and three down-payment tiers (strictly ascending). + Set markup percent (0-10), three down-payment tiers, and image mode. Validation: @@ -106,18 +106,9 @@ def update( `{percent_of_list (0–100), fixed_eur (≥0)}`. No strict-ascending check — the final EUR per tier is offer-dependent (`listino_imponibile * pct + eur`). - Persistence: - - - `agency_markup_percent` → `dealer_public.nlt_agency_percent` (rounded to int; - live column is `Integer NOT NULL DEFAULT 2`). - - `down_payment_tiers` → `dealer_public.nlt_anticipi_config` JSONB, stored in - apimax shape `[{"pct": <0..1>, "eur": }, ...]`. The partner-facing - `percent_of_list` (0–100) is divided by 100 to keep the column byte-compatible - with the DealerMAX UI calculator that reads the same JSONB. - - There is NO `vat_treatment` field: VAT is per-offer (`nlt_offerte.solo_privati`) - in the canonical DataMax pricing model, not per-dealer. The offer detail - endpoint surfaces it per row instead. + The stored economics are immediately used by DealerMAX's dealer-aware NLT + calculator. There is NO `vat_treatment` field: VAT is per-offer, not per-dealer. + The offer detail endpoint surfaces it per row instead. `Idempotency-Key` replay uses the shared endpoint helper; a re-applied identical PATCH is also a row-level no-op by construction. @@ -128,8 +119,8 @@ def update( No strict-ascending validation: the final EUR amount depends on the offer's list price (`tier.percent_of_list / 100 * listino_imponibile + tier.fixed_eur`), so a tier that looks larger by % can produce a smaller EUR on cheap vehicles. Label - semantics (low/medium/high) are advisory — apimax/DealerMAX UI treats the 3 - positions as opaque slots ordered by intent. + semantics (low/medium/high) are advisory — DealerMAX UI treats the 3 positions + as opaque slots ordered by intent. extra_headers: Send extra headers @@ -233,7 +224,7 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> NltSettings: """ - Set markup percent (0-10) and three down-payment tiers (strictly ascending). + Set markup percent (0-10), three down-payment tiers, and image mode. Validation: @@ -242,18 +233,9 @@ async def update( `{percent_of_list (0–100), fixed_eur (≥0)}`. No strict-ascending check — the final EUR per tier is offer-dependent (`listino_imponibile * pct + eur`). - Persistence: - - - `agency_markup_percent` → `dealer_public.nlt_agency_percent` (rounded to int; - live column is `Integer NOT NULL DEFAULT 2`). - - `down_payment_tiers` → `dealer_public.nlt_anticipi_config` JSONB, stored in - apimax shape `[{"pct": <0..1>, "eur": }, ...]`. The partner-facing - `percent_of_list` (0–100) is divided by 100 to keep the column byte-compatible - with the DealerMAX UI calculator that reads the same JSONB. - - There is NO `vat_treatment` field: VAT is per-offer (`nlt_offerte.solo_privati`) - in the canonical DataMax pricing model, not per-dealer. The offer detail - endpoint surfaces it per row instead. + The stored economics are immediately used by DealerMAX's dealer-aware NLT + calculator. There is NO `vat_treatment` field: VAT is per-offer, not per-dealer. + The offer detail endpoint surfaces it per row instead. `Idempotency-Key` replay uses the shared endpoint helper; a re-applied identical PATCH is also a row-level no-op by construction. @@ -264,8 +246,8 @@ async def update( No strict-ascending validation: the final EUR amount depends on the offer's list price (`tier.percent_of_list / 100 * listino_imponibile + tier.fixed_eur`), so a tier that looks larger by % can produce a smaller EUR on cheap vehicles. Label - semantics (low/medium/high) are advisory — apimax/DealerMAX UI treats the 3 - positions as opaque slots ordered by intent. + semantics (low/medium/high) are advisory — DealerMAX UI treats the 3 positions + as opaque slots ordered by intent. extra_headers: Send extra headers diff --git a/src/partnermax/resources/dealers/vehicles/images.py b/src/partnermax/resources/dealers/vehicles/images.py index 1e21756..b2aa56b 100644 --- a/src/partnermax/resources/dealers/vehicles/images.py +++ b/src/partnermax/resources/dealers/vehicles/images.py @@ -2,10 +2,13 @@ from __future__ import annotations +from typing import Mapping, cast + import httpx -from ...._types import Body, Query, Headers, NoneType, NotGiven, not_given -from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._files import deepcopy_with_paths +from ...._types import Body, Query, Headers, NoneType, NotGiven, FileTypes, not_given +from ...._utils import extract_files, path_template, maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -52,7 +55,7 @@ def create( vehicle_id: str, *, dealer_id: str, - file: str, + file: FileTypes, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -91,6 +94,8 @@ def create( raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") if not vehicle_id: raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") + body = deepcopy_with_paths({"file": file}, [["file"]]) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- @@ -99,7 +104,8 @@ def create( path_template( "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images", dealer_id=dealer_id, vehicle_id=vehicle_id ), - body=maybe_transform({"file": file}, image_create_params.ImageCreateParams), + body=maybe_transform(body, image_create_params.ImageCreateParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -122,8 +128,9 @@ def list( List every photo attached to a vehicle, ordered by `position`. No pagination — a vehicle is capped at 20 photos so the full list always fits in - a single response. `position=1` is the cover; use `DELETE` and re-`POST` to - re-order. + a single response. `position=1` is the cover. There is no single-image + retrieve/update route in v1: retrieve through this list and replace/re-order by + deleting and re-posting the affected images. Args: extra_headers: Send extra headers @@ -233,7 +240,7 @@ async def create( vehicle_id: str, *, dealer_id: str, - file: str, + file: FileTypes, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -272,6 +279,8 @@ async def create( raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") if not vehicle_id: raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") + body = deepcopy_with_paths({"file": file}, [["file"]]) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- @@ -280,7 +289,8 @@ async def create( path_template( "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images", dealer_id=dealer_id, vehicle_id=vehicle_id ), - body=await async_maybe_transform({"file": file}, image_create_params.ImageCreateParams), + body=await async_maybe_transform(body, image_create_params.ImageCreateParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -303,8 +313,9 @@ async def list( List every photo attached to a vehicle, ordered by `position`. No pagination — a vehicle is capped at 20 photos so the full list always fits in - a single response. `position=1` is the cover; use `DELETE` and re-`POST` to - re-order. + a single response. `position=1` is the cover. There is no single-image + retrieve/update route in v1: retrieve through this list and replace/re-order by + deleting and re-posting the affected images. Args: extra_headers: Send extra headers diff --git a/src/partnermax/resources/dealers/vehicles/vehicles.py b/src/partnermax/resources/dealers/vehicles/vehicles.py index 424941e..bd70e94 100644 --- a/src/partnermax/resources/dealers/vehicles/vehicles.py +++ b/src/partnermax/resources/dealers/vehicles/vehicles.py @@ -24,7 +24,8 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ...._base_client import make_request_options +from ....pagination import SyncCursorPage, AsyncCursorPage +from ...._base_client import AsyncPaginator, make_request_options from ....types.dealers import ( vehicle_bulk_params, vehicle_list_params, @@ -32,8 +33,8 @@ vehicle_update_params, vehicle_retrieve_params, ) -from ....types.dealers.vehicle_list import VehicleList from ....types.dealers.vehicle_detail import VehicleDetail +from ....types.dealers.vehicle_summary import VehicleSummary from ....types.dealers.bulk_create_vehicles_response import BulkCreateVehiclesResponse __all__ = ["VehiclesResource", "AsyncVehiclesResource"] @@ -104,25 +105,25 @@ def create( """ Provision a new used vehicle in a dealer's stock. - Writes are atomic across `azlease_usatoin` and `azlease_usatoauto` using a - `SAVEPOINT` so a UNIQUE plate violation rolls back cleanly. On success the - AI-content worker (:mod:`azurenet-engine.app.jobs.usato_ai_content_worker`) - picks up the new row within 60 seconds and generates the SEO body + pgvector - embedding — at which point the vehicle becomes discoverable on the cross-network + The write is atomic: a plate conflict or catalogue-code error leaves no partial + stock record behind. On success the asynchronous AI-content worker picks up the + new vehicle within 60 seconds and generates the SEO body plus semantic + embedding; at that point the vehicle becomes discoverable on the cross-network MCP / Custom GPT / NLWeb surfaces. The response returns immediately (no synchronous wait on the worker). Args: certified_km: Certified odometer reading at intake, in kilometres. - motornet_code: Motornet UNI code identifying the exact vehicle configuration. Must exist in - `mnet_dettagli_usato` at submission time; otherwise the call returns 422 - `motornet_code_not_in_catalogue`. The partner is expected to source this from - its own DMS; partnermax does not expose a plate→code lookup. + motornet_code: Motornet UNI code identifying the exact vehicle configuration. Must exist in the + used-vehicle catalogue at submission time; otherwise the call returns 422 + `motornet_code_not_in_catalogue`. Partners may send a code from their own + Motornet agreement or use the paid control-plane targa/VIN resolver before + creating the vehicle. plate: Italian licence plate. Uppercased server-side. UNIQUE across the network for - active vehicles (`visibile=true AND venduto_il IS NULL`); reusable once the - previous holder sells/hides the row. + active vehicles; reusable once the previous holder withdraws the vehicle from + sale. registration_year: Year of first registration. Upper bound is current year + 1. @@ -131,13 +132,12 @@ def create( description: Partner-supplied long description. Surfaced on the dealer site detail page. - is_for_sale: Maps to `azlease_usatoauto.is_vendita_enabled`. When false the row is in stock - but not offered for sale. + is_for_sale: When false the vehicle remains in stock but is not offered for sale. is_visible: Soft-publish flag. When false the row exists in stock but is excluded from - consumer-facing AI surfaces. Maps to `azlease_usatoin.visibile`. + consumer-facing AI surfaces. - notes: Free-form short notes; surfaced as `mnet_dettagli.precisazioni`-style. + notes: Free-form short notes for partner-facing vehicle detail views. registration_month: Month of registration (1–12). @@ -263,9 +263,9 @@ def update( """ Partial update of a vehicle. - Splits the inbound body across the two physical tables (`azlease_usatoauto` and - `azlease_usatoin`) and emits at most one UPDATE per table inside a single - transaction. Fields not present in the body are not touched. + Applies the transmitted fields inside a single transaction. Fields not present + in the body are not touched; explicit `null` clears only fields that are + nullable in the public contract. Args: extra_headers: Send extra headers @@ -322,7 +322,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> VehicleList: + ) -> SyncCursorPage[VehicleSummary]: """ List vehicles in a dealer's stock owned by the calling partner. @@ -349,8 +349,9 @@ def list( """ if not dealer_id: raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") - return self._get( + return self._get_api_list( path_template("/v1/dealers/{dealer_id}/vehicles", dealer_id=dealer_id), + page=SyncCursorPage[VehicleSummary], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -367,7 +368,7 @@ def list( vehicle_list_params.VehicleListParams, ), ), - cast_to=VehicleList, + model=VehicleSummary, ) def delete( @@ -385,18 +386,15 @@ def delete( """ Withdraw a vehicle from sale without deleting the row. - Sets `azlease_usatoin.visibile = FALSE` and stamps `venduto_il = now()`. The - plate becomes reusable on the network the moment this returns (the - active-uniqueness check excludes rows where `visibile = FALSE` OR - `venduto_il IS NOT NULL`). + Marks the vehicle as no longer for sale. The plate becomes reusable on the + network the moment this returns. Soft-delete is the canonical "remove this vehicle from sale" surface. The AI-citation consumers (MCP `_tool_search_vehicles`, Custom GPT - `search_vehicles_network`, NLWeb `/ask`) each filter their own queries on - `i.visibile = TRUE AND i.venduto_il IS NULL` — the shared `v_apimax_listing` - view itself does not impose that filter, every consumer adds it. The result on - the partner side is the same: a soft-deleted vehicle disappears from every AI - surface within the next index cycle. + `search_vehicles_network`, NLWeb `/ask`) each filter their own + public-availability state. The result on the partner side is the same: a + soft-deleted vehicle disappears from every AI surface within the next index + cycle. Returns `409 vehicle_already_deleted` if the row is already soft- deleted — same idempotency pattern as the dealers DELETE endpoint. @@ -551,25 +549,25 @@ async def create( """ Provision a new used vehicle in a dealer's stock. - Writes are atomic across `azlease_usatoin` and `azlease_usatoauto` using a - `SAVEPOINT` so a UNIQUE plate violation rolls back cleanly. On success the - AI-content worker (:mod:`azurenet-engine.app.jobs.usato_ai_content_worker`) - picks up the new row within 60 seconds and generates the SEO body + pgvector - embedding — at which point the vehicle becomes discoverable on the cross-network + The write is atomic: a plate conflict or catalogue-code error leaves no partial + stock record behind. On success the asynchronous AI-content worker picks up the + new vehicle within 60 seconds and generates the SEO body plus semantic + embedding; at that point the vehicle becomes discoverable on the cross-network MCP / Custom GPT / NLWeb surfaces. The response returns immediately (no synchronous wait on the worker). Args: certified_km: Certified odometer reading at intake, in kilometres. - motornet_code: Motornet UNI code identifying the exact vehicle configuration. Must exist in - `mnet_dettagli_usato` at submission time; otherwise the call returns 422 - `motornet_code_not_in_catalogue`. The partner is expected to source this from - its own DMS; partnermax does not expose a plate→code lookup. + motornet_code: Motornet UNI code identifying the exact vehicle configuration. Must exist in the + used-vehicle catalogue at submission time; otherwise the call returns 422 + `motornet_code_not_in_catalogue`. Partners may send a code from their own + Motornet agreement or use the paid control-plane targa/VIN resolver before + creating the vehicle. plate: Italian licence plate. Uppercased server-side. UNIQUE across the network for - active vehicles (`visibile=true AND venduto_il IS NULL`); reusable once the - previous holder sells/hides the row. + active vehicles; reusable once the previous holder withdraws the vehicle from + sale. registration_year: Year of first registration. Upper bound is current year + 1. @@ -578,13 +576,12 @@ async def create( description: Partner-supplied long description. Surfaced on the dealer site detail page. - is_for_sale: Maps to `azlease_usatoauto.is_vendita_enabled`. When false the row is in stock - but not offered for sale. + is_for_sale: When false the vehicle remains in stock but is not offered for sale. is_visible: Soft-publish flag. When false the row exists in stock but is excluded from - consumer-facing AI surfaces. Maps to `azlease_usatoin.visibile`. + consumer-facing AI surfaces. - notes: Free-form short notes; surfaced as `mnet_dettagli.precisazioni`-style. + notes: Free-form short notes for partner-facing vehicle detail views. registration_month: Month of registration (1–12). @@ -710,9 +707,9 @@ async def update( """ Partial update of a vehicle. - Splits the inbound body across the two physical tables (`azlease_usatoauto` and - `azlease_usatoin`) and emits at most one UPDATE per table inside a single - transaction. Fields not present in the body are not touched. + Applies the transmitted fields inside a single transaction. Fields not present + in the body are not touched; explicit `null` clears only fields that are + nullable in the public contract. Args: extra_headers: Send extra headers @@ -754,7 +751,7 @@ async def update( cast_to=VehicleDetail, ) - async def list( + def list( self, dealer_id: str, *, @@ -769,7 +766,7 @@ async def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> VehicleList: + ) -> AsyncPaginator[VehicleSummary, AsyncCursorPage[VehicleSummary]]: """ List vehicles in a dealer's stock owned by the calling partner. @@ -796,14 +793,15 @@ async def list( """ if not dealer_id: raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") - return await self._get( + return self._get_api_list( path_template("/v1/dealers/{dealer_id}/vehicles", dealer_id=dealer_id), + page=AsyncCursorPage[VehicleSummary], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform( + query=maybe_transform( { "cursor": cursor, "include_deleted": include_deleted, @@ -814,7 +812,7 @@ async def list( vehicle_list_params.VehicleListParams, ), ), - cast_to=VehicleList, + model=VehicleSummary, ) async def delete( @@ -832,18 +830,15 @@ async def delete( """ Withdraw a vehicle from sale without deleting the row. - Sets `azlease_usatoin.visibile = FALSE` and stamps `venduto_il = now()`. The - plate becomes reusable on the network the moment this returns (the - active-uniqueness check excludes rows where `visibile = FALSE` OR - `venduto_il IS NOT NULL`). + Marks the vehicle as no longer for sale. The plate becomes reusable on the + network the moment this returns. Soft-delete is the canonical "remove this vehicle from sale" surface. The AI-citation consumers (MCP `_tool_search_vehicles`, Custom GPT - `search_vehicles_network`, NLWeb `/ask`) each filter their own queries on - `i.visibile = TRUE AND i.venduto_il IS NULL` — the shared `v_apimax_listing` - view itself does not impose that filter, every consumer adds it. The result on - the partner side is the same: a soft-deleted vehicle disappears from every AI - surface within the next index cycle. + `search_vehicles_network`, NLWeb `/ask`) each filter their own + public-availability state. The result on the partner side is the same: a + soft-deleted vehicle disappears from every AI surface within the next index + cycle. Returns `409 vehicle_already_deleted` if the row is already soft- deleted — same idempotency pattern as the dealers DELETE endpoint. diff --git a/src/partnermax/resources/keys.py b/src/partnermax/resources/keys.py index b340bd6..aee931f 100644 --- a/src/partnermax/resources/keys.py +++ b/src/partnermax/resources/keys.py @@ -26,9 +26,9 @@ class KeysResource(SyncAPIResource): - """API key lifecycle management — issue, list, revoke. + """API key lifecycle management. - The partner authenticates every request with `X-Api-Key` (preferred) or `Authorization: Bearer `; the server identifies the partner from the key and scopes all reads/writes to dealers owned by that partner. + PartnerMAX v1 allows one active API key per partner/environment; partners authenticate every request with `X-Api-Key` (preferred) or `Authorization: Bearer `, and replacement is handled through DealerMAX support. """ @cached_property @@ -87,14 +87,14 @@ def issue( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> KeyIssueResponse: - """Issue a new API key. - - Plaintext returned exactly once. + """ + Rotate the partner API key during a DealerMAX support flow. - Capability gate: caller's key must hold `can_issue_keys`. The bootstrap key - handed to a partner by DealerMAX support carries this capability; keys minted - here inherit the _same_ capabilities as the caller, so partners can never - accidentally widen their own scope. + Plaintext is returned exactly once. The key authenticating this request is + deactivated in the same transaction that inserts the replacement, preserving + PartnerMAX v1's one-active-key invariant. Callers must not use this endpoint for + per-deployment, CI, or engineer credentials. Capability gate: caller's key must + hold `can_issue_keys`. Args: label: Human-readable identifier for this key, used for safe logging. @@ -162,9 +162,9 @@ def revoke( class AsyncKeysResource(AsyncAPIResource): - """API key lifecycle management — issue, list, revoke. + """API key lifecycle management. - The partner authenticates every request with `X-Api-Key` (preferred) or `Authorization: Bearer `; the server identifies the partner from the key and scopes all reads/writes to dealers owned by that partner. + PartnerMAX v1 allows one active API key per partner/environment; partners authenticate every request with `X-Api-Key` (preferred) or `Authorization: Bearer `, and replacement is handled through DealerMAX support. """ @cached_property @@ -223,14 +223,14 @@ async def issue( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> KeyIssueResponse: - """Issue a new API key. - - Plaintext returned exactly once. + """ + Rotate the partner API key during a DealerMAX support flow. - Capability gate: caller's key must hold `can_issue_keys`. The bootstrap key - handed to a partner by DealerMAX support carries this capability; keys minted - here inherit the _same_ capabilities as the caller, so partners can never - accidentally widen their own scope. + Plaintext is returned exactly once. The key authenticating this request is + deactivated in the same transaction that inserts the replacement, preserving + PartnerMAX v1's one-active-key invariant. Callers must not use this endpoint for + per-deployment, CI, or engineer credentials. Capability gate: caller's key must + hold `can_issue_keys`. Args: label: Human-readable identifier for this key, used for safe logging. diff --git a/src/partnermax/types/__init__.py b/src/partnermax/types/__init__.py index 4aa4a08..58dd7c2 100644 --- a/src/partnermax/types/__init__.py +++ b/src/partnermax/types/__init__.py @@ -8,6 +8,4 @@ from .key_list_response import KeyListResponse as KeyListResponse from .dealer_list_params import DealerListParams as DealerListParams from .key_issue_response import KeyIssueResponse as KeyIssueResponse -from .dealer_create_params import DealerCreateParams as DealerCreateParams -from .dealer_list_response import DealerListResponse as DealerListResponse from .dealer_update_params import DealerUpdateParams as DealerUpdateParams diff --git a/src/partnermax/types/dealer_create_params.py b/src/partnermax/types/dealer_create_params.py deleted file mode 100644 index 91e5534..0000000 --- a/src/partnermax/types/dealer_create_params.py +++ /dev/null @@ -1,36 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict -from typing_extensions import Required, Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["DealerCreateParams"] - - -class DealerCreateParams(TypedDict, total=False): - address: Required[str] - - business_name: Required[str] - - city: Required[str] - - contact_email: Required[str] - - contact_phone: Required[str] - - postal_code: Required[str] - - primary_domain: Required[str] - - province_code: Required[str] - - vat_number: Required[str] - - activate: bool - - metadata: Dict[str, str] - - idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] diff --git a/src/partnermax/types/dealer_list_response.py b/src/partnermax/types/dealer_list_response.py deleted file mode 100644 index 436dc2e..0000000 --- a/src/partnermax/types/dealer_list_response.py +++ /dev/null @@ -1,18 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from .._models import BaseModel -from .dealer_summary import DealerSummary - -__all__ = ["DealerListResponse"] - - -class DealerListResponse(BaseModel): - """Response envelope for ``GET /v1/dealers``.""" - - data: List[DealerSummary] - - has_more: bool - - next_cursor: Optional[str] = None diff --git a/src/partnermax/types/dealers/bulk_row_outcome.py b/src/partnermax/types/dealers/bulk_row_outcome.py index 83d8b98..e04e7f9 100644 --- a/src/partnermax/types/dealers/bulk_row_outcome.py +++ b/src/partnermax/types/dealers/bulk_row_outcome.py @@ -43,18 +43,14 @@ class BulkRowOutcome(BaseModel): - **Partner-supplied** — what the partner posted (`plate`, `description`, `sale_price_eur`, etc.). - - **Catalogue-derived** — `technical_details` is the flat `mnet_dettagli_usato` - dict (Italian column keys: `cilindrata`, `kw`, `hp`, `lunghezza`, - `consumo_medio`, `emissioni_co2`, etc.). Same shape conventions as - `NltOfferDetail` per `feedback_partnermax_field_naming_us_english`. + - **Catalogue-derived** — `technical_details` is a flat dictionary of + Motornet-backed technical attributes using Italian domain labels such as + `cilindrata`, `kw`, `hp`, `lunghezza`, `consumo_medio`, and `emissioni_co2`. - **AI-derived** — `ai_content` carries the editorial output the cross-network consumers display (descriptions, highlights, FAQ, SEO meta). `null` until the worker has processed the vehicle. - Fields the partner does NOT see through this surface (because they are - dealer-internal margin/operations data the partner does not own): - `cost_price_eur`, `inspection_expiry_date`, `road_tax_expiry_date`, - `previous_owner_count`, `previous_ownership_transfer_date`, `last_service_*`. - These exist in the underlying DB tables for the DealerMAX dashboard but are - intentionally not exposed via the SDK. + Dealer-internal margin and operations data remains outside this SDK surface; + partners receive only the inventory, commercial, catalogue, media, and + AI-derived fields needed to publish the vehicle. """ diff --git a/src/partnermax/types/dealers/down_payment_tiers.py b/src/partnermax/types/dealers/down_payment_tiers.py index eb528d8..291e35e 100644 --- a/src/partnermax/types/dealers/down_payment_tiers.py +++ b/src/partnermax/types/dealers/down_payment_tiers.py @@ -8,15 +8,10 @@ class High(BaseModel): """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to - a deal is `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number - (UI-friendly: write `12.5`, not `0.125`); the router persists - `pct = percent_of_list / 100` to stay byte-compatible with the - DealerMAX UI calculator. + (UI-friendly: write `12.5`, not `0.125`). The final EUR applied to + a deal is computed from the offer's IVA-excluded list price plus the + flat component. """ fixed_eur: int @@ -35,15 +30,10 @@ class High(BaseModel): class Low(BaseModel): """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to - a deal is `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number - (UI-friendly: write `12.5`, not `0.125`); the router persists - `pct = percent_of_list / 100` to stay byte-compatible with the - DealerMAX UI calculator. + (UI-friendly: write `12.5`, not `0.125`). The final EUR applied to + a deal is computed from the offer's IVA-excluded list price plus the + flat component. """ fixed_eur: int @@ -62,15 +52,10 @@ class Low(BaseModel): class Medium(BaseModel): """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to - a deal is `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number - (UI-friendly: write `12.5`, not `0.125`); the router persists - `pct = percent_of_list / 100` to stay byte-compatible with the - DealerMAX UI calculator. + (UI-friendly: write `12.5`, not `0.125`). The final EUR applied to + a deal is computed from the offer's IVA-excluded list price plus the + flat component. """ fixed_eur: int @@ -93,45 +78,30 @@ class DownPaymentTiers(BaseModel): the offer's list price (`tier.percent_of_list / 100 * listino_imponibile + tier.fixed_eur`), so a tier that looks larger by % can produce a smaller EUR on cheap vehicles. Label - semantics (low/medium/high) are advisory — apimax/DealerMAX UI + semantics (low/medium/high) are advisory — DealerMAX UI treats the 3 positions as opaque slots ordered by intent. """ high: High """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is - `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write - `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay - byte-compatible with the DealerMAX UI calculator. + `12.5`, not `0.125`). The final EUR applied to a deal is computed from the + offer's IVA-excluded list price plus the flat component. """ low: Low """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is - `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write - `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay - byte-compatible with the DealerMAX UI calculator. + `12.5`, not `0.125`). The final EUR applied to a deal is computed from the + offer's IVA-excluded list price plus the flat component. """ medium: Medium """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is - `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write - `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay - byte-compatible with the DealerMAX UI calculator. + `12.5`, not `0.125`). The final EUR applied to a deal is computed from the + offer's IVA-excluded list price plus the flat component. """ diff --git a/src/partnermax/types/dealers/down_payment_tiers_param.py b/src/partnermax/types/dealers/down_payment_tiers_param.py index 6c0d5fe..f780592 100644 --- a/src/partnermax/types/dealers/down_payment_tiers_param.py +++ b/src/partnermax/types/dealers/down_payment_tiers_param.py @@ -10,15 +10,10 @@ class High(TypedDict, total=False): """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to - a deal is `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number - (UI-friendly: write `12.5`, not `0.125`); the router persists - `pct = percent_of_list / 100` to stay byte-compatible with the - DealerMAX UI calculator. + (UI-friendly: write `12.5`, not `0.125`). The final EUR applied to + a deal is computed from the offer's IVA-excluded list price plus the + flat component. """ fixed_eur: Required[int] @@ -37,15 +32,10 @@ class High(TypedDict, total=False): class Low(TypedDict, total=False): """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to - a deal is `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number - (UI-friendly: write `12.5`, not `0.125`); the router persists - `pct = percent_of_list / 100` to stay byte-compatible with the - DealerMAX UI calculator. + (UI-friendly: write `12.5`, not `0.125`). The final EUR applied to + a deal is computed from the offer's IVA-excluded list price plus the + flat component. """ fixed_eur: Required[int] @@ -64,15 +54,10 @@ class Low(TypedDict, total=False): class Medium(TypedDict, total=False): """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to - a deal is `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number - (UI-friendly: write `12.5`, not `0.125`); the router persists - `pct = percent_of_list / 100` to stay byte-compatible with the - DealerMAX UI calculator. + (UI-friendly: write `12.5`, not `0.125`). The final EUR applied to + a deal is computed from the offer's IVA-excluded list price plus the + flat component. """ fixed_eur: Required[int] @@ -95,45 +80,30 @@ class DownPaymentTiersParam(TypedDict, total=False): the offer's list price (`tier.percent_of_list / 100 * listino_imponibile + tier.fixed_eur`), so a tier that looks larger by % can produce a smaller EUR on cheap vehicles. Label - semantics (low/medium/high) are advisory — apimax/DealerMAX UI + semantics (low/medium/high) are advisory — DealerMAX UI treats the 3 positions as opaque slots ordered by intent. """ high: Required[High] """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is - `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write - `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay - byte-compatible with the DealerMAX UI calculator. + `12.5`, not `0.125`). The final EUR applied to a deal is computed from the + offer's IVA-excluded list price plus the flat component. """ low: Required[Low] """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is - `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write - `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay - byte-compatible with the DealerMAX UI calculator. + `12.5`, not `0.125`). The final EUR applied to a deal is computed from the + offer's IVA-excluded list price plus the flat component. """ medium: Required[Medium] """One down-payment tier — percent of list price + flat EUR. - apimax: `dealer_public.nlt_anticipi_config` is a JSONB list of three - `{"pct": <0..1>, "eur": }` entries. The final EUR applied to a deal is - `listino_imponibile * pct + eur` (see - `apimax/app/services/nlt/calculator.py::calcola_anticipo_eur`). - Partnermax API exposes `percent_of_list` as a 0–100 number (UI-friendly: write - `12.5`, not `0.125`); the router persists `pct = percent_of_list / 100` to stay - byte-compatible with the DealerMAX UI calculator. + `12.5`, not `0.125`). The final EUR applied to a deal is computed from the + offer's IVA-excluded list price plus the flat component. """ diff --git a/src/partnermax/types/dealers/nlt/__init__.py b/src/partnermax/types/dealers/nlt/__init__.py index 935a2a9..33c7327 100644 --- a/src/partnermax/types/dealers/nlt/__init__.py +++ b/src/partnermax/types/dealers/nlt/__init__.py @@ -4,5 +4,4 @@ from .nlt_offer_summary import NltOfferSummary as NltOfferSummary from .offer_list_params import OfferListParams as OfferListParams -from .offer_list_response import OfferListResponse as OfferListResponse from .offer_retrieve_response import OfferRetrieveResponse as OfferRetrieveResponse diff --git a/src/partnermax/types/dealers/nlt/nlt_offer_summary.py b/src/partnermax/types/dealers/nlt/nlt_offer_summary.py index e6721c8..7c6255b 100644 --- a/src/partnermax/types/dealers/nlt/nlt_offer_summary.py +++ b/src/partnermax/types/dealers/nlt/nlt_offer_summary.py @@ -11,11 +11,11 @@ class NltOfferSummary(BaseModel): """Single row in the offers list. Pricing is dealer-aware. - Field names: American English snake_case. Values: Italian raw, - apimax-aligned (`fuel_type: "Benzina"`, `segment: "SUV piccoli"`). - No enum normalization — apimax labels are surfaced verbatim, exactly - as the detail endpoint does, so the partner client sees the same - string in both listing and detail. + Field names: American English snake_case. Values use DealerMAX's Italian + catalogue vocabulary (`fuel_type: "Benzina"`, `segment: "SUV piccoli"`). + No enum normalization — labels are surfaced verbatim, exactly as the detail + endpoint does, so the partner client sees the same string in both listing + and detail. """ brand: str diff --git a/src/partnermax/types/dealers/nlt/offer_list_response.py b/src/partnermax/types/dealers/nlt/offer_list_response.py deleted file mode 100644 index 2ffed76..0000000 --- a/src/partnermax/types/dealers/nlt/offer_list_response.py +++ /dev/null @@ -1,18 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from ...._models import BaseModel -from .nlt_offer_summary import NltOfferSummary - -__all__ = ["OfferListResponse"] - - -class OfferListResponse(BaseModel): - """Cursor-paginated list of offer summaries.""" - - data: List[NltOfferSummary] - - has_more: bool - - next_cursor: Optional[str] = None diff --git a/src/partnermax/types/dealers/nlt/offer_retrieve_response.py b/src/partnermax/types/dealers/nlt/offer_retrieve_response.py index a247a73..e93444a 100644 --- a/src/partnermax/types/dealers/nlt/offer_retrieve_response.py +++ b/src/partnermax/types/dealers/nlt/offer_retrieve_response.py @@ -24,8 +24,7 @@ class AvailableAddonsReplacementVehicle(BaseModel): - """ - Replacement-vehicle add-on lookup (apimax: `addons_disponibili.auto_sostitutiva`). + """Replacement-vehicle add-on lookup. Always category B (utilitaria) per founder decision — the spoken "average customer" segment. @@ -39,12 +38,11 @@ class AvailableAddonsReplacementVehicle(BaseModel): class AvailableAddonsTires(BaseModel): - """Tyre-replacement add-on lookup (apimax: `addons_disponibili.pneumatici`). + """Tyre-replacement add-on lookup. - Populated when `mnet_dettagli.pneumatici_anteriori` matches `R\\dd+` and - a row exists in `nlt_pneumatici` for that diameter. Null otherwise. - Replacement rule (founder decision 2026-05-12): 1 set of 4 tyres - every 30 000 km, rounded up. + Populated when the catalogue carries a parseable tyre diameter and a + DealerMAX-managed tyre package exists for that diameter. Null otherwise. + Replacement rule: 1 set of 4 tyres every 30 000 km, rounded up. """ diameter_in: int @@ -55,31 +53,29 @@ class AvailableAddonsTires(BaseModel): class AvailableAddons(BaseModel): - """Container for optional add-ons (apimax: `addons_disponibili`).""" + """Container for optional add-ons.""" replacement_vehicle: Optional[AvailableAddonsReplacementVehicle] = None - """ - Replacement-vehicle add-on lookup (apimax: - `addons_disponibili.auto_sostitutiva`). + """Replacement-vehicle add-on lookup. Always category B (utilitaria) per founder decision — the spoken "average customer" segment. """ tires: Optional[AvailableAddonsTires] = None - """Tyre-replacement add-on lookup (apimax: `addons_disponibili.pneumatici`). + """Tyre-replacement add-on lookup. - Populated when `mnet_dettagli.pneumatici_anteriori` matches `R\\dd+` and a row - exists in `nlt_pneumatici` for that diameter. Null otherwise. Replacement rule - (founder decision 2026-05-12): 1 set of 4 tyres every 30 000 km, rounded up. + Populated when the catalogue carries a parseable tyre diameter and a + DealerMAX-managed tyre package exists for that diameter. Null otherwise. + Replacement rule: 1 set of 4 tyres every 30 000 km, rounded up. """ class DownPaymentScenariosEur(BaseModel): """Three down-payment scenarios in EUR (whole amounts). - apimax: `anticipo_scenari_eur` (keys remapped to American English - snake_case for partnermax SDK: `zero/medium/standard`). + Keys use American-English snake_case for the partnermax SDK: + `zero`, `medium`, `standard`. """ medium: int @@ -92,9 +88,8 @@ class DownPaymentScenariosEur(BaseModel): class DownPaymentScenariosLabels(BaseModel): """Italian labels paired 1:1 with `NltDownPaymentScenariosEur`. - apimax: `anticipo_scenari_labels` — used by Custom GPT to render the - three options in conversation. Values stay in Italian raw - ("Senza anticipo" / "Anticipo 12,5%" / "Anticipo 25%"). + Values stay in Italian raw ("Senza anticipo" / "Anticipo 12,5%" / + "Anticipo 25%") so partner UIs match DealerMAX consumer-facing copy. """ medium: str @@ -107,10 +102,9 @@ class DownPaymentScenariosLabels(BaseModel): class Faq(BaseModel): """One Italian Q&A entry derived per-offer. - apimax: `build_offer_faqs` in `seo_engine/nlt_faq_builder.py` — - generates up to ~11 Q&A pairs (dimensions, fuel, transmission, CO2, - monthly canon at preset combo, available durations, VAT inclusion, - down-payment tiers, etc.). Partnermax surfaces them all, 1:1. + Generated from the same grounded offer payload used by DealerMAX + consumer-facing surfaces: dimensions, fuel, transmission, CO2, monthly + canon, available durations, VAT inclusion, down-payment tiers, etc. """ answer: str @@ -119,7 +113,7 @@ class Faq(BaseModel): class Gallery(BaseModel): - """One image in the offer gallery (apimax: `gallery[]`).""" + """One image in the offer gallery.""" is_cover: bool @@ -127,7 +121,7 @@ class Gallery(BaseModel): class IncludedAccessory(BaseModel): - """One accessory bundled with the offer (apimax: `accessori_inclusi[]`).""" + """One accessory bundled with the offer.""" code: str @@ -139,9 +133,7 @@ class IncludedAccessory(BaseModel): class IncludedService(BaseModel): """One NLT service normally included in the canone. - apimax: `_get_services_included` (`nlt_resolver.py:719`). Source is - the global `nlt_services` table (active rows only). Same set of - services across the network (Assicurazione RCA / Kasco / + Same set of services across the network (Assicurazione RCA / Kasco / Incendio-Furto, Manutenzione, Assistenza Stradale, Bollo, Pneumatici, Veicolo in anticipo, Vettura sostitutiva). Not per-offer. """ @@ -152,15 +144,15 @@ class IncludedService(BaseModel): class NetworkOffer(BaseModel): - """One network dealer's quote for this offer (apimax: `network_offers[]`). + """One network dealer's quote for this offer. - Sorted by `min_monthly_canon_eur ASC`. In partnermax this list is - scoped to dealers owned by the calling partner - (`utenti.parent_id = partner.user_id`) — same shape as the apimax - cross-network list, partner-scoped to avoid data leakage. + Sorted by `min_monthly_canon_eur ASC`. In PartnerMAX this list is scoped + to the calling partner's `partner_dealers` rows and returns the + partner-owned `external_dealer_id`. Legacy `dlr_` values remain only + for compatibility callers. """ - dealer_id: int + dealer_id: str dealer_name: str @@ -184,10 +176,9 @@ class NetworkOffer(BaseModel): class Quotation(BaseModel): """One priced cell of the 18-combination matrix. - apimax: `quotazioni[]` entry — `_compute_quotazioni_dealer_aware` - (mcp_server.py:180). Reflects the dealer's vetrina formula applied to - each (durata, km) combo; cells with implausible canon (<€50) are - dropped upstream, so the list may contain fewer than 18 rows. + Reflects the dealer-aware pricing formula applied to each + (duration, yearly-km) combination. Cells with implausible canons are + filtered upstream, so the list may contain fewer than 18 rows. """ duration_months: int @@ -198,10 +189,9 @@ class Quotation(BaseModel): class Tag(BaseModel): - """Category tag for an offer (apimax: `tags[]`). + """Category tag for an offer. - Populated from `nlt_offerta_tag` ⋈ `nlt_offerte_tag`. Examples in - production: "Promo", "Stock pronto", "GreenChoice". + Examples in production: "Promo", "Stock pronto", "GreenChoice". """ name: str @@ -214,11 +204,9 @@ class Tag(BaseModel): class OfferRetrieveResponse(BaseModel): """Full offer detail. - Shape mirrors `_tool_get_nlt_offer_details` (apimax MCP) with all - field names translated to American English snake_case for the - partner SDK contract. VALUES stay Italian raw (apimax-aligned). - The dict `technical_details` keeps Italian KEYS because they are - `mnet_dettagli` column names (raw DB). + Field names use American-English snake_case for the partner SDK contract. + Values stay Italian raw where the underlying automotive catalogue uses + Italian labels. The dict `technical_details` keeps Italian domain keys. """ found: bool @@ -234,7 +222,7 @@ class OfferRetrieveResponse(BaseModel): vat_included: bool available_addons: Optional[AvailableAddons] = None - """Container for optional add-ons (apimax: `addons_disponibili`).""" + """Container for optional add-ons.""" brand: Optional[str] = None @@ -245,16 +233,15 @@ class OfferRetrieveResponse(BaseModel): down_payment_scenarios_eur: Optional[DownPaymentScenariosEur] = None """Three down-payment scenarios in EUR (whole amounts). - apimax: `anticipo_scenari_eur` (keys remapped to American English snake_case for - partnermax SDK: `zero/medium/standard`). + Keys use American-English snake_case for the partnermax SDK: `zero`, `medium`, + `standard`. """ down_payment_scenarios_labels: Optional[DownPaymentScenariosLabels] = None """Italian labels paired 1:1 with `NltDownPaymentScenariosEur`. - apimax: `anticipo_scenari_labels` — used by Custom GPT to render the three - options in conversation. Values stay in Italian raw ("Senza anticipo" / - "Anticipo 12,5%" / "Anticipo 25%"). + Values stay in Italian raw ("Senza anticipo" / "Anticipo 12,5%" / "Anticipo + 25%") so partner UIs match DealerMAX consumer-facing copy. """ faqs: Optional[List[Faq]] = None diff --git a/src/partnermax/types/dealers/nlt_setting_update_params.py b/src/partnermax/types/dealers/nlt_setting_update_params.py index 3c513c4..ad7e73b 100644 --- a/src/partnermax/types/dealers/nlt_setting_update_params.py +++ b/src/partnermax/types/dealers/nlt_setting_update_params.py @@ -20,8 +20,8 @@ class NltSettingUpdateParams(TypedDict, total=False): No strict-ascending validation: the final EUR amount depends on the offer's list price (`tier.percent_of_list / 100 * listino_imponibile + tier.fixed_eur`), so a tier that looks larger by % can produce a smaller EUR on cheap vehicles. Label - semantics (low/medium/high) are advisory — apimax/DealerMAX UI treats the 3 - positions as opaque slots ordered by intent. + semantics (low/medium/high) are advisory — DealerMAX UI treats the 3 positions + as opaque slots ordered by intent. """ currency: Literal["EUR"] diff --git a/src/partnermax/types/dealers/nlt_settings.py b/src/partnermax/types/dealers/nlt_settings.py index b1cecac..1b2adeb 100644 --- a/src/partnermax/types/dealers/nlt_settings.py +++ b/src/partnermax/types/dealers/nlt_settings.py @@ -14,8 +14,8 @@ class NltSettings(BaseModel): """Response model for GET / PATCH /v1/dealers/{id}/nlt-settings. Note: there is no `vat_treatment` field — VAT is a property of the - offer (`nlt_offerte.solo_privati`), not of the dealer. The offer - detail returns the VAT treatment per row instead. + offer, not of the dealer. The offer detail returns the VAT treatment + per row instead. """ agency_markup_percent: float @@ -28,8 +28,8 @@ class NltSettings(BaseModel): No strict-ascending validation: the final EUR amount depends on the offer's list price (`tier.percent_of_list / 100 * listino_imponibile + tier.fixed_eur`), so a tier that looks larger by % can produce a smaller EUR on cheap vehicles. Label - semantics (low/medium/high) are advisory — apimax/DealerMAX UI treats the 3 - positions as opaque slots ordered by intent. + semantics (low/medium/high) are advisory — DealerMAX UI treats the 3 positions + as opaque slots ordered by intent. """ effective_from: datetime diff --git a/src/partnermax/types/dealers/vehicle_bulk_params.py b/src/partnermax/types/dealers/vehicle_bulk_params.py index 35578ad..dc24e2d 100644 --- a/src/partnermax/types/dealers/vehicle_bulk_params.py +++ b/src/partnermax/types/dealers/vehicle_bulk_params.py @@ -27,10 +27,8 @@ class Vehicle(TypedDict, total=False): The partner sends a small, vehicle-specific payload. All technical specs (brand, model, trim, fuel type, displacement, dimensions, CO2, etc.) are - derived server-side from `mnet_dettagli` via the `motornet_code` join — - the partner never types them. This is the same canonical pattern used by - NLT offers and matches the platform rule - `feedback_motornet_authoritative`. + derived server-side from DealerMAX's licensed Motornet-backed catalogue — + the partner never types them. Fields immutable after creation: `motornet_code`, `plate`, `vin`. Other fields may be updated via PATCH. @@ -42,17 +40,17 @@ class Vehicle(TypedDict, total=False): motornet_code: Required[str] """Motornet UNI code identifying the exact vehicle configuration. - Must exist in `mnet_dettagli_usato` at submission time; otherwise the call - returns 422 `motornet_code_not_in_catalogue`. The partner is expected to source - this from its own DMS; partnermax does not expose a plate→code lookup. + Must exist in the used-vehicle catalogue at submission time; otherwise the call + returns 422 `motornet_code_not_in_catalogue`. Partners may send a code from + their own Motornet agreement or use the paid control-plane targa/VIN resolver + before creating the vehicle. """ plate: Required[str] """Italian licence plate. - Uppercased server-side. UNIQUE across the network for active vehicles - (`visibile=true AND venduto_il IS NULL`); reusable once the previous holder - sells/hides the row. + Uppercased server-side. UNIQUE across the network for active vehicles; reusable + once the previous holder withdraws the vehicle from sale. """ registration_year: Required[int] @@ -77,20 +75,17 @@ class Vehicle(TypedDict, total=False): extended_warranty_months: Optional[int] is_for_sale: bool - """Maps to `azlease_usatoauto.is_vendita_enabled`. - - When false the row is in stock but not offered for sale. - """ + """When false the vehicle remains in stock but is not offered for sale.""" is_visible: bool """Soft-publish flag. When false the row exists in stock but is excluded from consumer-facing AI - surfaces. Maps to `azlease_usatoin.visibile`. + surfaces. """ notes: Optional[str] - """Free-form short notes; surfaced as `mnet_dettagli.precisazioni`-style.""" + """Free-form short notes for partner-facing vehicle detail views.""" registration_month: Optional[int] """Month of registration (1–12).""" diff --git a/src/partnermax/types/dealers/vehicle_create_params.py b/src/partnermax/types/dealers/vehicle_create_params.py index 6dfda24..d1d4075 100644 --- a/src/partnermax/types/dealers/vehicle_create_params.py +++ b/src/partnermax/types/dealers/vehicle_create_params.py @@ -17,17 +17,17 @@ class VehicleCreateParams(TypedDict, total=False): motornet_code: Required[str] """Motornet UNI code identifying the exact vehicle configuration. - Must exist in `mnet_dettagli_usato` at submission time; otherwise the call - returns 422 `motornet_code_not_in_catalogue`. The partner is expected to source - this from its own DMS; partnermax does not expose a plate→code lookup. + Must exist in the used-vehicle catalogue at submission time; otherwise the call + returns 422 `motornet_code_not_in_catalogue`. Partners may send a code from + their own Motornet agreement or use the paid control-plane targa/VIN resolver + before creating the vehicle. """ plate: Required[str] """Italian licence plate. - Uppercased server-side. UNIQUE across the network for active vehicles - (`visibile=true AND venduto_il IS NULL`); reusable once the previous holder - sells/hides the row. + Uppercased server-side. UNIQUE across the network for active vehicles; reusable + once the previous holder withdraws the vehicle from sale. """ registration_year: Required[int] @@ -52,20 +52,17 @@ class VehicleCreateParams(TypedDict, total=False): extended_warranty_months: Optional[int] is_for_sale: bool - """Maps to `azlease_usatoauto.is_vendita_enabled`. - - When false the row is in stock but not offered for sale. - """ + """When false the vehicle remains in stock but is not offered for sale.""" is_visible: bool """Soft-publish flag. When false the row exists in stock but is excluded from consumer-facing AI - surfaces. Maps to `azlease_usatoin.visibile`. + surfaces. """ notes: Optional[str] - """Free-form short notes; surfaced as `mnet_dettagli.precisazioni`-style.""" + """Free-form short notes for partner-facing vehicle detail views.""" registration_month: Optional[int] """Month of registration (1–12).""" diff --git a/src/partnermax/types/dealers/vehicle_detail.py b/src/partnermax/types/dealers/vehicle_detail.py index a243d04..afff02b 100644 --- a/src/partnermax/types/dealers/vehicle_detail.py +++ b/src/partnermax/types/dealers/vehicle_detail.py @@ -19,21 +19,17 @@ class VehicleDetail(BaseModel): * **Partner-supplied** — what the partner posted (``plate``, ``description``, ``sale_price_eur``, etc.). - * **Catalogue-derived** — ``technical_details`` is the flat - ``mnet_dettagli_usato`` dict (Italian column keys: ``cilindrata``, ``kw``, - ``hp``, ``lunghezza``, ``consumo_medio``, ``emissioni_co2``, etc.). - Same shape conventions as ``NltOfferDetail`` per - ``feedback_partnermax_field_naming_us_english``. + * **Catalogue-derived** — ``technical_details`` is a flat dictionary of + Motornet-backed technical attributes using Italian domain labels such as + ``cilindrata``, ``kw``, ``hp``, ``lunghezza``, ``consumo_medio``, and + ``emissioni_co2``. * **AI-derived** — ``ai_content`` carries the editorial output the cross-network consumers display (descriptions, highlights, FAQ, SEO meta). ``null`` until the worker has processed the vehicle. - Fields the partner does NOT see through this surface (because they - are dealer-internal margin/operations data the partner does not own): - ``cost_price_eur``, ``inspection_expiry_date``, ``road_tax_expiry_date``, - ``previous_owner_count``, ``previous_ownership_transfer_date``, - ``last_service_*``. These exist in the underlying DB tables for the - DealerMAX dashboard but are intentionally not exposed via the SDK. + Dealer-internal margin and operations data remains outside this SDK + surface; partners receive only the inventory, commercial, catalogue, media, + and AI-derived fields needed to publish the vehicle. """ certified_km: int @@ -99,10 +95,11 @@ class VehicleDetail(BaseModel): registration_month: Optional[int] = None technical_details: Optional[Dict[str, object]] = None - """ - Flat dict of every non-null `mnet_dettagli_usato` column for this - `motornet_code`. Keys stay in Italian because they are raw SQL column names; - native units preserved (mm, kg, kW, CV, g/km, etc.). + """Flat dictionary of Motornet-backed technical attributes for this + `motornet_code`. + + Keys stay in Italian domain vocabulary; native units are preserved (mm, kg, kW, + CV, g/km, etc.). """ trim: Optional[str] = None diff --git a/src/partnermax/types/dealers/vehicle_summary.py b/src/partnermax/types/dealers/vehicle_summary.py index 50d90f2..81fb90d 100644 --- a/src/partnermax/types/dealers/vehicle_summary.py +++ b/src/partnermax/types/dealers/vehicle_summary.py @@ -12,8 +12,9 @@ class VehicleSummary(BaseModel): """Compact vehicle payload for list endpoints. Catalogue fields (`brand`, `model`, `trim`, `fuel_type`) are derived from - `mnet_dettagli` at read time. Italian raw labels are surfaced verbatim — - same convention as NLT (`apimax`-aligned). + DealerMAX's licensed Motornet-backed catalogue at read time. Italian raw + labels are surfaced verbatim so partner clients see the same vocabulary as + consumer-facing DealerMAX surfaces. """ certified_km: int diff --git a/src/partnermax/types/dealers/vehicles/image_create_params.py b/src/partnermax/types/dealers/vehicles/image_create_params.py index 567d5d2..1f284d3 100644 --- a/src/partnermax/types/dealers/vehicles/image_create_params.py +++ b/src/partnermax/types/dealers/vehicles/image_create_params.py @@ -4,13 +4,15 @@ from typing_extensions import Required, TypedDict +from ...._types import FileTypes + __all__ = ["ImageCreateParams"] class ImageCreateParams(TypedDict, total=False): dealer_id: Required[str] - file: Required[str] + file: Required[FileTypes] """The photo file. JPEG, PNG, or WebP, up to 15 MB. WebP is converted to PNG server-side. diff --git a/tests/api_resources/dealers/nlt/test_offers.py b/tests/api_resources/dealers/nlt/test_offers.py index c1b5355..e00e7df 100644 --- a/tests/api_resources/dealers/nlt/test_offers.py +++ b/tests/api_resources/dealers/nlt/test_offers.py @@ -9,7 +9,8 @@ from partnermax import Partnermax, AsyncPartnermax from tests.utils import assert_matches_type -from partnermax.types.dealers.nlt import OfferListResponse, OfferRetrieveResponse +from partnermax.pagination import SyncCursorPage, AsyncCursorPage +from partnermax.types.dealers.nlt import NltOfferSummary, OfferRetrieveResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -75,7 +76,7 @@ def test_method_list(self, client: Partnermax) -> None: offer = client.dealers.nlt.offers.list( dealer_id="dealer_id", ) - assert_matches_type(OfferListResponse, offer, path=["response"]) + assert_matches_type(SyncCursorPage[NltOfferSummary], offer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -92,7 +93,7 @@ def test_method_list_with_all_params(self, client: Partnermax) -> None: segment="segment", vehicle_type="vcom", ) - assert_matches_type(OfferListResponse, offer, path=["response"]) + assert_matches_type(SyncCursorPage[NltOfferSummary], offer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -104,7 +105,7 @@ def test_raw_response_list(self, client: Partnermax) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" offer = response.parse() - assert_matches_type(OfferListResponse, offer, path=["response"]) + assert_matches_type(SyncCursorPage[NltOfferSummary], offer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -116,7 +117,7 @@ def test_streaming_response_list(self, client: Partnermax) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" offer = response.parse() - assert_matches_type(OfferListResponse, offer, path=["response"]) + assert_matches_type(SyncCursorPage[NltOfferSummary], offer, path=["response"]) assert cast(Any, response.is_closed) is True @@ -192,7 +193,7 @@ async def test_method_list(self, async_client: AsyncPartnermax) -> None: offer = await async_client.dealers.nlt.offers.list( dealer_id="dealer_id", ) - assert_matches_type(OfferListResponse, offer, path=["response"]) + assert_matches_type(AsyncCursorPage[NltOfferSummary], offer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -209,7 +210,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncPartnermax) segment="segment", vehicle_type="vcom", ) - assert_matches_type(OfferListResponse, offer, path=["response"]) + assert_matches_type(AsyncCursorPage[NltOfferSummary], offer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -221,7 +222,7 @@ async def test_raw_response_list(self, async_client: AsyncPartnermax) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" offer = await response.parse() - assert_matches_type(OfferListResponse, offer, path=["response"]) + assert_matches_type(AsyncCursorPage[NltOfferSummary], offer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -233,7 +234,7 @@ async def test_streaming_response_list(self, async_client: AsyncPartnermax) -> N assert response.http_request.headers.get("X-Stainless-Lang") == "python" offer = await response.parse() - assert_matches_type(OfferListResponse, offer, path=["response"]) + assert_matches_type(AsyncCursorPage[NltOfferSummary], offer, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/dealers/test_vehicles.py b/tests/api_resources/dealers/test_vehicles.py index ddd1183..265a4e0 100644 --- a/tests/api_resources/dealers/test_vehicles.py +++ b/tests/api_resources/dealers/test_vehicles.py @@ -9,9 +9,10 @@ from partnermax import Partnermax, AsyncPartnermax from tests.utils import assert_matches_type +from partnermax.pagination import SyncCursorPage, AsyncCursorPage from partnermax.types.dealers import ( - VehicleList, VehicleDetail, + VehicleSummary, BulkCreateVehiclesResponse, ) @@ -252,7 +253,7 @@ def test_method_list(self, client: Partnermax) -> None: vehicle = client.dealers.vehicles.list( dealer_id="dealer_id", ) - assert_matches_type(VehicleList, vehicle, path=["response"]) + assert_matches_type(SyncCursorPage[VehicleSummary], vehicle, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -265,7 +266,7 @@ def test_method_list_with_all_params(self, client: Partnermax) -> None: is_visible=True, limit=1, ) - assert_matches_type(VehicleList, vehicle, path=["response"]) + assert_matches_type(SyncCursorPage[VehicleSummary], vehicle, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -277,7 +278,7 @@ def test_raw_response_list(self, client: Partnermax) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" vehicle = response.parse() - assert_matches_type(VehicleList, vehicle, path=["response"]) + assert_matches_type(SyncCursorPage[VehicleSummary], vehicle, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -289,7 +290,7 @@ def test_streaming_response_list(self, client: Partnermax) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" vehicle = response.parse() - assert_matches_type(VehicleList, vehicle, path=["response"]) + assert_matches_type(SyncCursorPage[VehicleSummary], vehicle, path=["response"]) assert cast(Any, response.is_closed) is True @@ -698,7 +699,7 @@ async def test_method_list(self, async_client: AsyncPartnermax) -> None: vehicle = await async_client.dealers.vehicles.list( dealer_id="dealer_id", ) - assert_matches_type(VehicleList, vehicle, path=["response"]) + assert_matches_type(AsyncCursorPage[VehicleSummary], vehicle, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -711,7 +712,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncPartnermax) is_visible=True, limit=1, ) - assert_matches_type(VehicleList, vehicle, path=["response"]) + assert_matches_type(AsyncCursorPage[VehicleSummary], vehicle, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -723,7 +724,7 @@ async def test_raw_response_list(self, async_client: AsyncPartnermax) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" vehicle = await response.parse() - assert_matches_type(VehicleList, vehicle, path=["response"]) + assert_matches_type(AsyncCursorPage[VehicleSummary], vehicle, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -735,7 +736,7 @@ async def test_streaming_response_list(self, async_client: AsyncPartnermax) -> N assert response.http_request.headers.get("X-Stainless-Lang") == "python" vehicle = await response.parse() - assert_matches_type(VehicleList, vehicle, path=["response"]) + assert_matches_type(AsyncCursorPage[VehicleSummary], vehicle, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/dealers/vehicles/test_images.py b/tests/api_resources/dealers/vehicles/test_images.py index 46f33dc..37084fb 100644 --- a/tests/api_resources/dealers/vehicles/test_images.py +++ b/tests/api_resources/dealers/vehicles/test_images.py @@ -23,7 +23,7 @@ def test_method_create(self, client: Partnermax) -> None: image = client.dealers.vehicles.images.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) assert_matches_type(VehicleImage, image, path=["response"]) @@ -33,7 +33,7 @@ def test_raw_response_create(self, client: Partnermax) -> None: response = client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) assert response.is_closed is True @@ -47,7 +47,7 @@ def test_streaming_response_create(self, client: Partnermax) -> None: with client.dealers.vehicles.images.with_streaming_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -64,14 +64,14 @@ def test_path_params_create(self, client: Partnermax) -> None: client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="", - file="file", + file=b"Example data", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): client.dealers.vehicles.images.with_raw_response.create( vehicle_id="", dealer_id="dealer_id", - file="file", + file=b"Example data", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -202,7 +202,7 @@ async def test_method_create(self, async_client: AsyncPartnermax) -> None: image = await async_client.dealers.vehicles.images.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) assert_matches_type(VehicleImage, image, path=["response"]) @@ -212,7 +212,7 @@ async def test_raw_response_create(self, async_client: AsyncPartnermax) -> None: response = await async_client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) assert response.is_closed is True @@ -226,7 +226,7 @@ async def test_streaming_response_create(self, async_client: AsyncPartnermax) -> async with async_client.dealers.vehicles.images.with_streaming_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -243,14 +243,14 @@ async def test_path_params_create(self, async_client: AsyncPartnermax) -> None: await async_client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="", - file="file", + file=b"Example data", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): await async_client.dealers.vehicles.images.with_raw_response.create( vehicle_id="", dealer_id="dealer_id", - file="file", + file=b"Example data", ) @pytest.mark.skip(reason="Mock server tests are disabled") diff --git a/tests/api_resources/test_dealers.py b/tests/api_resources/test_dealers.py index dd5d3fa..4fb1a7c 100644 --- a/tests/api_resources/test_dealers.py +++ b/tests/api_resources/test_dealers.py @@ -9,10 +9,10 @@ from partnermax import Partnermax, AsyncPartnermax from tests.utils import assert_matches_type -from partnermax.types import ( - DealerDetail, - DealerListResponse, -) +from partnermax.types import DealerDetail, DealerSummary +from partnermax.pagination import SyncCursorPage, AsyncCursorPage + +# pyright: reportDeprecated=false base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -20,97 +20,23 @@ class TestDealers: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_create(self, client: Partnermax) -> None: - dealer = client.dealers.create( - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - postal_code="21029", - primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", - province_code="SE", - vat_number="IT21029798095", - ) - assert_matches_type(DealerDetail, dealer, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: Partnermax) -> None: - dealer = client.dealers.create( - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - postal_code="21029", - primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", - province_code="SE", - vat_number="IT21029798095", - activate=True, - metadata={"foo": "string"}, - idempotency_key="Idempotency-Key", - ) - assert_matches_type(DealerDetail, dealer, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_create(self, client: Partnermax) -> None: - response = client.dealers.with_raw_response.create( - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - postal_code="21029", - primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", - province_code="SE", - vat_number="IT21029798095", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_create(self, client: Partnermax) -> None: - with client.dealers.with_streaming_response.create( - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - postal_code="21029", - primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", - province_code="SE", - vat_number="IT21029798095", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - dealer = response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) - - assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Partnermax) -> None: - dealer = client.dealers.retrieve( - "dealer_id", - ) + with pytest.warns(DeprecationWarning): + dealer = client.dealers.retrieve( + "dealer_id", + ) + assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Partnermax) -> None: - response = client.dealers.with_raw_response.retrieve( - "dealer_id", - ) + with pytest.warns(DeprecationWarning): + response = client.dealers.with_raw_response.retrieve( + "dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -120,57 +46,64 @@ def test_raw_response_retrieve(self, client: Partnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Partnermax) -> None: - with client.dealers.with_streaming_response.retrieve( - "dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.dealers.with_streaming_response.retrieve( + "dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) + dealer = response.parse() + assert_matches_type(DealerDetail, dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Partnermax) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - client.dealers.with_raw_response.retrieve( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.with_raw_response.retrieve( + "", + ) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update(self, client: Partnermax) -> None: - dealer = client.dealers.update( - dealer_id="dealer_id", - ) + with pytest.warns(DeprecationWarning): + dealer = client.dealers.update( + dealer_id="dealer_id", + ) + assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update_with_all_params(self, client: Partnermax) -> None: - dealer = client.dealers.update( - dealer_id="dealer_id", - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - metadata={"foo": "string"}, - postal_code="21029", - province_code="SE", - status="active", - idempotency_key="Idempotency-Key", - ) + with pytest.warns(DeprecationWarning): + dealer = client.dealers.update( + dealer_id="dealer_id", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + metadata={"foo": "string"}, + postal_code="21029", + province_code="SE", + status="active", + idempotency_key="Idempotency-Key", + ) + assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_update(self, client: Partnermax) -> None: - response = client.dealers.with_raw_response.update( - dealer_id="dealer_id", - ) + with pytest.warns(DeprecationWarning): + response = client.dealers.with_raw_response.update( + dealer_id="dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -180,77 +113,88 @@ def test_raw_response_update(self, client: Partnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_update(self, client: Partnermax) -> None: - with client.dealers.with_streaming_response.update( - dealer_id="dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.dealers.with_streaming_response.update( + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) + dealer = response.parse() + assert_matches_type(DealerDetail, dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_update(self, client: Partnermax) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - client.dealers.with_raw_response.update( - dealer_id="", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.with_raw_response.update( + dealer_id="", + ) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Partnermax) -> None: - dealer = client.dealers.list() - assert_matches_type(DealerListResponse, dealer, path=["response"]) + with pytest.warns(DeprecationWarning): + dealer = client.dealers.list() + + assert_matches_type(SyncCursorPage[DealerSummary], dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Partnermax) -> None: - dealer = client.dealers.list( - cursor="cursor", - limit=1, - status="active", - ) - assert_matches_type(DealerListResponse, dealer, path=["response"]) + with pytest.warns(DeprecationWarning): + dealer = client.dealers.list( + cursor="cursor", + limit=1, + status="active", + ) + + assert_matches_type(SyncCursorPage[DealerSummary], dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Partnermax) -> None: - response = client.dealers.with_raw_response.list() + with pytest.warns(DeprecationWarning): + response = client.dealers.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" dealer = response.parse() - assert_matches_type(DealerListResponse, dealer, path=["response"]) + assert_matches_type(SyncCursorPage[DealerSummary], dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Partnermax) -> None: - with client.dealers.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.dealers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = response.parse() - assert_matches_type(DealerListResponse, dealer, path=["response"]) + dealer = response.parse() + assert_matches_type(SyncCursorPage[DealerSummary], dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete(self, client: Partnermax) -> None: - dealer = client.dealers.delete( - "dealer_id", - ) + with pytest.warns(DeprecationWarning): + dealer = client.dealers.delete( + "dealer_id", + ) + assert dealer is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete(self, client: Partnermax) -> None: - response = client.dealers.with_raw_response.delete( - "dealer_id", - ) + with pytest.warns(DeprecationWarning): + response = client.dealers.with_raw_response.delete( + "dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -260,24 +204,26 @@ def test_raw_response_delete(self, client: Partnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete(self, client: Partnermax) -> None: - with client.dealers.with_streaming_response.delete( - "dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.dealers.with_streaming_response.delete( + "dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = response.parse() - assert dealer is None + dealer = response.parse() + assert dealer is None assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete(self, client: Partnermax) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - client.dealers.with_raw_response.delete( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.with_raw_response.delete( + "", + ) class TestAsyncDealers: @@ -285,97 +231,23 @@ class TestAsyncDealers: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncPartnermax) -> None: - dealer = await async_client.dealers.create( - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - postal_code="21029", - primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", - province_code="SE", - vat_number="IT21029798095", - ) - assert_matches_type(DealerDetail, dealer, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncPartnermax) -> None: - dealer = await async_client.dealers.create( - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - postal_code="21029", - primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", - province_code="SE", - vat_number="IT21029798095", - activate=True, - metadata={"foo": "string"}, - idempotency_key="Idempotency-Key", - ) - assert_matches_type(DealerDetail, dealer, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncPartnermax) -> None: - response = await async_client.dealers.with_raw_response.create( - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - postal_code="21029", - primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", - province_code="SE", - vat_number="IT21029798095", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = await response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncPartnermax) -> None: - async with async_client.dealers.with_streaming_response.create( - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - postal_code="21029", - primary_domain="29-0.mi-57.16u-2d91-aha.o-l7c19m0.z.9zhin-8ja2-7.447----86.6.61--5-6.2.3-i2al.r.name", - province_code="SE", - vat_number="IT21029798095", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - dealer = await response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) - - assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncPartnermax) -> None: - dealer = await async_client.dealers.retrieve( - "dealer_id", - ) + with pytest.warns(DeprecationWarning): + dealer = await async_client.dealers.retrieve( + "dealer_id", + ) + assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncPartnermax) -> None: - response = await async_client.dealers.with_raw_response.retrieve( - "dealer_id", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.dealers.with_raw_response.retrieve( + "dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -385,57 +257,64 @@ async def test_raw_response_retrieve(self, async_client: AsyncPartnermax) -> Non @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncPartnermax) -> None: - async with async_client.dealers.with_streaming_response.retrieve( - "dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.dealers.with_streaming_response.retrieve( + "dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = await response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) + dealer = await response.parse() + assert_matches_type(DealerDetail, dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncPartnermax) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - await async_client.dealers.with_raw_response.retrieve( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.with_raw_response.retrieve( + "", + ) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update(self, async_client: AsyncPartnermax) -> None: - dealer = await async_client.dealers.update( - dealer_id="dealer_id", - ) + with pytest.warns(DeprecationWarning): + dealer = await async_client.dealers.update( + dealer_id="dealer_id", + ) + assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update_with_all_params(self, async_client: AsyncPartnermax) -> None: - dealer = await async_client.dealers.update( - dealer_id="dealer_id", - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - metadata={"foo": "string"}, - postal_code="21029", - province_code="SE", - status="active", - idempotency_key="Idempotency-Key", - ) + with pytest.warns(DeprecationWarning): + dealer = await async_client.dealers.update( + dealer_id="dealer_id", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + metadata={"foo": "string"}, + postal_code="21029", + province_code="SE", + status="active", + idempotency_key="Idempotency-Key", + ) + assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_update(self, async_client: AsyncPartnermax) -> None: - response = await async_client.dealers.with_raw_response.update( - dealer_id="dealer_id", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.dealers.with_raw_response.update( + dealer_id="dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -445,77 +324,88 @@ async def test_raw_response_update(self, async_client: AsyncPartnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_update(self, async_client: AsyncPartnermax) -> None: - async with async_client.dealers.with_streaming_response.update( - dealer_id="dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.dealers.with_streaming_response.update( + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = await response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) + dealer = await response.parse() + assert_matches_type(DealerDetail, dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_update(self, async_client: AsyncPartnermax) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - await async_client.dealers.with_raw_response.update( - dealer_id="", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.with_raw_response.update( + dealer_id="", + ) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncPartnermax) -> None: - dealer = await async_client.dealers.list() - assert_matches_type(DealerListResponse, dealer, path=["response"]) + with pytest.warns(DeprecationWarning): + dealer = await async_client.dealers.list() + + assert_matches_type(AsyncCursorPage[DealerSummary], dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncPartnermax) -> None: - dealer = await async_client.dealers.list( - cursor="cursor", - limit=1, - status="active", - ) - assert_matches_type(DealerListResponse, dealer, path=["response"]) + with pytest.warns(DeprecationWarning): + dealer = await async_client.dealers.list( + cursor="cursor", + limit=1, + status="active", + ) + + assert_matches_type(AsyncCursorPage[DealerSummary], dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncPartnermax) -> None: - response = await async_client.dealers.with_raw_response.list() + with pytest.warns(DeprecationWarning): + response = await async_client.dealers.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" dealer = await response.parse() - assert_matches_type(DealerListResponse, dealer, path=["response"]) + assert_matches_type(AsyncCursorPage[DealerSummary], dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncPartnermax) -> None: - async with async_client.dealers.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.dealers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = await response.parse() - assert_matches_type(DealerListResponse, dealer, path=["response"]) + dealer = await response.parse() + assert_matches_type(AsyncCursorPage[DealerSummary], dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncPartnermax) -> None: - dealer = await async_client.dealers.delete( - "dealer_id", - ) + with pytest.warns(DeprecationWarning): + dealer = await async_client.dealers.delete( + "dealer_id", + ) + assert dealer is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncPartnermax) -> None: - response = await async_client.dealers.with_raw_response.delete( - "dealer_id", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.dealers.with_raw_response.delete( + "dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -525,21 +415,23 @@ async def test_raw_response_delete(self, async_client: AsyncPartnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncPartnermax) -> None: - async with async_client.dealers.with_streaming_response.delete( - "dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.dealers.with_streaming_response.delete( + "dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = await response.parse() - assert dealer is None + dealer = await response.parse() + assert dealer is None assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete(self, async_client: AsyncPartnermax) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - await async_client.dealers.with_raw_response.delete( - "", - ) + with pytest.warns(DeprecationWarning): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.with_raw_response.delete( + "", + ) diff --git a/tests/test_client.py b/tests/test_client.py index 89e4426..303c588 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,7 +23,7 @@ from partnermax._types import Omit from partnermax._utils import asyncify from partnermax._models import BaseModel, FinalRequestOptions -from partnermax._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError +from partnermax._exceptions import APIStatusError, APIResponseValidationError from partnermax._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, @@ -103,14 +103,6 @@ async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] yield item -def _get_open_connections(client: Partnermax | AsyncPartnermax) -> int: - transport = client._client._transport - assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) - - pool = transport._pool - return len(pool._requests) - - class TestPartnermax: @pytest.mark.respx(base_url=base_url) def test_raw_response(self, respx_mock: MockRouter, client: Partnermax) -> None: @@ -899,25 +891,6 @@ def test_parse_retry_after_header( calculated = client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - @mock.patch("partnermax._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Partnermax) -> None: - respx_mock.get("/v1/dealers").mock(side_effect=httpx.TimeoutException("Test timeout error")) - - with pytest.raises(APITimeoutError): - client.dealers.with_streaming_response.list().__enter__() - - assert _get_open_connections(client) == 0 - - @mock.patch("partnermax._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Partnermax) -> None: - respx_mock.get("/v1/dealers").mock(return_value=httpx.Response(500)) - - with pytest.raises(APIStatusError): - client.dealers.with_streaming_response.list().__enter__() - assert _get_open_connections(client) == 0 - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("partnermax._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -1850,29 +1823,6 @@ async def test_parse_retry_after_header( calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - @mock.patch("partnermax._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - async def test_retrying_timeout_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncPartnermax - ) -> None: - respx_mock.get("/v1/dealers").mock(side_effect=httpx.TimeoutException("Test timeout error")) - - with pytest.raises(APITimeoutError): - await async_client.dealers.with_streaming_response.list().__aenter__() - - assert _get_open_connections(async_client) == 0 - - @mock.patch("partnermax._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - async def test_retrying_status_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncPartnermax - ) -> None: - respx_mock.get("/v1/dealers").mock(return_value=httpx.Response(500)) - - with pytest.raises(APIStatusError): - await async_client.dealers.with_streaming_response.list().__aenter__() - assert _get_open_connections(async_client) == 0 - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("partnermax._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) From 62bdc98e17d9222d23199332da41a40cc78f4dfc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:59:41 +0000 Subject: [PATCH 05/10] fix: regenerate SDK with active dealer methods --- .stats.yml | 4 +- README.md | 19 - src/partnermax/_files.py | 2 +- src/partnermax/resources/dealers/dealers.py | 105 ++---- .../resources/dealers/vehicles/images.py | 21 +- .../dealers/vehicles/image_create_params.py | 4 +- .../dealers/vehicles/test_images.py | 20 +- tests/api_resources/test_dealers.py | 352 ++++++++---------- 8 files changed, 204 insertions(+), 323 deletions(-) diff --git a/.stats.yml b/.stats.yml index b84d635..d02b966 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-12484954a328c4cc183eb761854de199b41e2b40ffcfb788217db027941820d1.yml -openapi_spec_hash: e9a8ecc3b1b9bffadec88bab183338a4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-fa55dc70ae5286c71a0faea047e23dfbb93be0bbfa3d7a0776d06c12a376444a.yml +openapi_spec_hash: 841e984b131bfb9b83d26fdd9dbd5303 config_hash: 410d10ad3652f4fdf97d2bd563de0d64 diff --git a/README.md b/README.md index 4051510..1f2c679 100644 --- a/README.md +++ b/README.md @@ -224,25 +224,6 @@ nlt_settings = client.dealers.nlt_settings.update( print(nlt_settings.down_payment_tiers) ``` -## File uploads - -Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. - -```python -from pathlib import Path -from partnermax import Partnermax - -client = Partnermax() - -client.dealers.vehicles.images.create( - vehicle_id="vehicle_id", - dealer_id="dealer_id", - file=Path("/path/to/file"), -) -``` - -The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. - ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `partnermax.APIConnectionError` is raised. diff --git a/src/partnermax/_files.py b/src/partnermax/_files.py index d962650..76da9e0 100644 --- a/src/partnermax/_files.py +++ b/src/partnermax/_files.py @@ -36,7 +36,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: if not is_file_content(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/DealerMax-app/partnermax-python/tree/main#file-uploads" + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." ) from None diff --git a/src/partnermax/resources/dealers/dealers.py b/src/partnermax/resources/dealers/dealers.py index 188d588..3c4a720 100644 --- a/src/partnermax/resources/dealers/dealers.py +++ b/src/partnermax/resources/dealers/dealers.py @@ -2,7 +2,6 @@ from __future__ import annotations -import typing_extensions from typing import Dict, Optional from typing_extensions import Literal @@ -89,7 +88,6 @@ def with_streaming_response(self) -> DealersResourceWithStreamingResponse: """ return DealersResourceWithStreamingResponse(self) - @typing_extensions.deprecated("deprecated") def retrieve( self, dealer_id: str, @@ -124,7 +122,6 @@ def retrieve( cast_to=DealerDetail, ) - @typing_extensions.deprecated("deprecated") def update( self, dealer_id: str, @@ -184,7 +181,6 @@ def update( cast_to=DealerDetail, ) - @typing_extensions.deprecated("deprecated") def list( self, *, @@ -231,7 +227,6 @@ def list( model=DealerSummary, ) - @typing_extensions.deprecated("deprecated") def delete( self, dealer_id: str, @@ -306,7 +301,6 @@ def with_streaming_response(self) -> AsyncDealersResourceWithStreamingResponse: """ return AsyncDealersResourceWithStreamingResponse(self) - @typing_extensions.deprecated("deprecated") async def retrieve( self, dealer_id: str, @@ -341,7 +335,6 @@ async def retrieve( cast_to=DealerDetail, ) - @typing_extensions.deprecated("deprecated") async def update( self, dealer_id: str, @@ -401,7 +394,6 @@ async def update( cast_to=DealerDetail, ) - @typing_extensions.deprecated("deprecated") def list( self, *, @@ -448,7 +440,6 @@ def list( model=DealerSummary, ) - @typing_extensions.deprecated("deprecated") async def delete( self, dealer_id: str, @@ -489,25 +480,17 @@ class DealersResourceWithRawResponse: def __init__(self, dealers: DealersResource) -> None: self._dealers = dealers - self.retrieve = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - dealers.retrieve, # pyright: ignore[reportDeprecated], - ) + self.retrieve = to_raw_response_wrapper( + dealers.retrieve, ) - self.update = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - dealers.update, # pyright: ignore[reportDeprecated], - ) + self.update = to_raw_response_wrapper( + dealers.update, ) - self.list = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - dealers.list, # pyright: ignore[reportDeprecated], - ) + self.list = to_raw_response_wrapper( + dealers.list, ) - self.delete = ( # pyright: ignore[reportDeprecated] - to_raw_response_wrapper( - dealers.delete, # pyright: ignore[reportDeprecated], - ) + self.delete = to_raw_response_wrapper( + dealers.delete, ) @cached_property @@ -531,25 +514,17 @@ class AsyncDealersResourceWithRawResponse: def __init__(self, dealers: AsyncDealersResource) -> None: self._dealers = dealers - self.retrieve = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - dealers.retrieve, # pyright: ignore[reportDeprecated], - ) + self.retrieve = async_to_raw_response_wrapper( + dealers.retrieve, ) - self.update = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - dealers.update, # pyright: ignore[reportDeprecated], - ) + self.update = async_to_raw_response_wrapper( + dealers.update, ) - self.list = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - dealers.list, # pyright: ignore[reportDeprecated], - ) + self.list = async_to_raw_response_wrapper( + dealers.list, ) - self.delete = ( # pyright: ignore[reportDeprecated] - async_to_raw_response_wrapper( - dealers.delete, # pyright: ignore[reportDeprecated], - ) + self.delete = async_to_raw_response_wrapper( + dealers.delete, ) @cached_property @@ -573,25 +548,17 @@ class DealersResourceWithStreamingResponse: def __init__(self, dealers: DealersResource) -> None: self._dealers = dealers - self.retrieve = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - dealers.retrieve, # pyright: ignore[reportDeprecated], - ) + self.retrieve = to_streamed_response_wrapper( + dealers.retrieve, ) - self.update = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - dealers.update, # pyright: ignore[reportDeprecated], - ) + self.update = to_streamed_response_wrapper( + dealers.update, ) - self.list = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - dealers.list, # pyright: ignore[reportDeprecated], - ) + self.list = to_streamed_response_wrapper( + dealers.list, ) - self.delete = ( # pyright: ignore[reportDeprecated] - to_streamed_response_wrapper( - dealers.delete, # pyright: ignore[reportDeprecated], - ) + self.delete = to_streamed_response_wrapper( + dealers.delete, ) @cached_property @@ -615,25 +582,17 @@ class AsyncDealersResourceWithStreamingResponse: def __init__(self, dealers: AsyncDealersResource) -> None: self._dealers = dealers - self.retrieve = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - dealers.retrieve, # pyright: ignore[reportDeprecated], - ) + self.retrieve = async_to_streamed_response_wrapper( + dealers.retrieve, ) - self.update = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - dealers.update, # pyright: ignore[reportDeprecated], - ) + self.update = async_to_streamed_response_wrapper( + dealers.update, ) - self.list = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - dealers.list, # pyright: ignore[reportDeprecated], - ) + self.list = async_to_streamed_response_wrapper( + dealers.list, ) - self.delete = ( # pyright: ignore[reportDeprecated] - async_to_streamed_response_wrapper( - dealers.delete, # pyright: ignore[reportDeprecated], - ) + self.delete = async_to_streamed_response_wrapper( + dealers.delete, ) @cached_property diff --git a/src/partnermax/resources/dealers/vehicles/images.py b/src/partnermax/resources/dealers/vehicles/images.py index b2aa56b..ed6c489 100644 --- a/src/partnermax/resources/dealers/vehicles/images.py +++ b/src/partnermax/resources/dealers/vehicles/images.py @@ -2,13 +2,10 @@ from __future__ import annotations -from typing import Mapping, cast - import httpx -from ...._files import deepcopy_with_paths -from ...._types import Body, Query, Headers, NoneType, NotGiven, FileTypes, not_given -from ...._utils import extract_files, path_template, maybe_transform, async_maybe_transform +from ...._types import Body, Query, Headers, NoneType, NotGiven, not_given +from ...._utils import path_template, maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -55,7 +52,7 @@ def create( vehicle_id: str, *, dealer_id: str, - file: FileTypes, + file: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -94,8 +91,6 @@ def create( raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") if not vehicle_id: raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") - body = deepcopy_with_paths({"file": file}, [["file"]]) - files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- @@ -104,8 +99,7 @@ def create( path_template( "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images", dealer_id=dealer_id, vehicle_id=vehicle_id ), - body=maybe_transform(body, image_create_params.ImageCreateParams), - files=files, + body=maybe_transform({"file": file}, image_create_params.ImageCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -240,7 +234,7 @@ async def create( vehicle_id: str, *, dealer_id: str, - file: FileTypes, + file: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -279,8 +273,6 @@ async def create( raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") if not vehicle_id: raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") - body = deepcopy_with_paths({"file": file}, [["file"]]) - files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- @@ -289,8 +281,7 @@ async def create( path_template( "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images", dealer_id=dealer_id, vehicle_id=vehicle_id ), - body=await async_maybe_transform(body, image_create_params.ImageCreateParams), - files=files, + body=await async_maybe_transform({"file": file}, image_create_params.ImageCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/partnermax/types/dealers/vehicles/image_create_params.py b/src/partnermax/types/dealers/vehicles/image_create_params.py index 1f284d3..567d5d2 100644 --- a/src/partnermax/types/dealers/vehicles/image_create_params.py +++ b/src/partnermax/types/dealers/vehicles/image_create_params.py @@ -4,15 +4,13 @@ from typing_extensions import Required, TypedDict -from ...._types import FileTypes - __all__ = ["ImageCreateParams"] class ImageCreateParams(TypedDict, total=False): dealer_id: Required[str] - file: Required[FileTypes] + file: Required[str] """The photo file. JPEG, PNG, or WebP, up to 15 MB. WebP is converted to PNG server-side. diff --git a/tests/api_resources/dealers/vehicles/test_images.py b/tests/api_resources/dealers/vehicles/test_images.py index 37084fb..46f33dc 100644 --- a/tests/api_resources/dealers/vehicles/test_images.py +++ b/tests/api_resources/dealers/vehicles/test_images.py @@ -23,7 +23,7 @@ def test_method_create(self, client: Partnermax) -> None: image = client.dealers.vehicles.images.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file=b"Example data", + file="file", ) assert_matches_type(VehicleImage, image, path=["response"]) @@ -33,7 +33,7 @@ def test_raw_response_create(self, client: Partnermax) -> None: response = client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file=b"Example data", + file="file", ) assert response.is_closed is True @@ -47,7 +47,7 @@ def test_streaming_response_create(self, client: Partnermax) -> None: with client.dealers.vehicles.images.with_streaming_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file=b"Example data", + file="file", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -64,14 +64,14 @@ def test_path_params_create(self, client: Partnermax) -> None: client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="", - file=b"Example data", + file="file", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): client.dealers.vehicles.images.with_raw_response.create( vehicle_id="", dealer_id="dealer_id", - file=b"Example data", + file="file", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -202,7 +202,7 @@ async def test_method_create(self, async_client: AsyncPartnermax) -> None: image = await async_client.dealers.vehicles.images.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file=b"Example data", + file="file", ) assert_matches_type(VehicleImage, image, path=["response"]) @@ -212,7 +212,7 @@ async def test_raw_response_create(self, async_client: AsyncPartnermax) -> None: response = await async_client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file=b"Example data", + file="file", ) assert response.is_closed is True @@ -226,7 +226,7 @@ async def test_streaming_response_create(self, async_client: AsyncPartnermax) -> async with async_client.dealers.vehicles.images.with_streaming_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file=b"Example data", + file="file", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -243,14 +243,14 @@ async def test_path_params_create(self, async_client: AsyncPartnermax) -> None: await async_client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="", - file=b"Example data", + file="file", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): await async_client.dealers.vehicles.images.with_raw_response.create( vehicle_id="", dealer_id="dealer_id", - file=b"Example data", + file="file", ) @pytest.mark.skip(reason="Mock server tests are disabled") diff --git a/tests/api_resources/test_dealers.py b/tests/api_resources/test_dealers.py index 4fb1a7c..b607716 100644 --- a/tests/api_resources/test_dealers.py +++ b/tests/api_resources/test_dealers.py @@ -12,8 +12,6 @@ from partnermax.types import DealerDetail, DealerSummary from partnermax.pagination import SyncCursorPage, AsyncCursorPage -# pyright: reportDeprecated=false - base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -23,20 +21,17 @@ class TestDealers: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = client.dealers.retrieve( - "dealer_id", - ) - + dealer = client.dealers.retrieve( + "dealer_id", + ) assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - response = client.dealers.with_raw_response.retrieve( - "dealer_id", - ) + response = client.dealers.with_raw_response.retrieve( + "dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -46,64 +41,57 @@ def test_raw_response_retrieve(self, client: Partnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - with client.dealers.with_streaming_response.retrieve( - "dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with client.dealers.with_streaming_response.retrieve( + "dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) + dealer = response.parse() + assert_matches_type(DealerDetail, dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_retrieve(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - client.dealers.with_raw_response.retrieve( - "", - ) + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.with_raw_response.retrieve( + "", + ) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = client.dealers.update( - dealer_id="dealer_id", - ) - + dealer = client.dealers.update( + dealer_id="dealer_id", + ) assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_update_with_all_params(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = client.dealers.update( - dealer_id="dealer_id", - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - metadata={"foo": "string"}, - postal_code="21029", - province_code="SE", - status="active", - idempotency_key="Idempotency-Key", - ) - + dealer = client.dealers.update( + dealer_id="dealer_id", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + metadata={"foo": "string"}, + postal_code="21029", + province_code="SE", + status="active", + idempotency_key="Idempotency-Key", + ) assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_update(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - response = client.dealers.with_raw_response.update( - dealer_id="dealer_id", - ) + response = client.dealers.with_raw_response.update( + dealer_id="dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -113,52 +101,45 @@ def test_raw_response_update(self, client: Partnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_update(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - with client.dealers.with_streaming_response.update( - dealer_id="dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with client.dealers.with_streaming_response.update( + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) + dealer = response.parse() + assert_matches_type(DealerDetail, dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_update(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - client.dealers.with_raw_response.update( - dealer_id="", - ) + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.with_raw_response.update( + dealer_id="", + ) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = client.dealers.list() - + dealer = client.dealers.list() assert_matches_type(SyncCursorPage[DealerSummary], dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_with_all_params(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = client.dealers.list( - cursor="cursor", - limit=1, - status="active", - ) - + dealer = client.dealers.list( + cursor="cursor", + limit=1, + status="active", + ) assert_matches_type(SyncCursorPage[DealerSummary], dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - response = client.dealers.with_raw_response.list() + response = client.dealers.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -168,33 +149,29 @@ def test_raw_response_list(self, client: Partnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - with client.dealers.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with client.dealers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = response.parse() - assert_matches_type(SyncCursorPage[DealerSummary], dealer, path=["response"]) + dealer = response.parse() + assert_matches_type(SyncCursorPage[DealerSummary], dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_delete(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = client.dealers.delete( - "dealer_id", - ) - + dealer = client.dealers.delete( + "dealer_id", + ) assert dealer is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_delete(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - response = client.dealers.with_raw_response.delete( - "dealer_id", - ) + response = client.dealers.with_raw_response.delete( + "dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -204,26 +181,24 @@ def test_raw_response_delete(self, client: Partnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_delete(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - with client.dealers.with_streaming_response.delete( - "dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with client.dealers.with_streaming_response.delete( + "dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = response.parse() - assert dealer is None + dealer = response.parse() + assert dealer is None assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_delete(self, client: Partnermax) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - client.dealers.with_raw_response.delete( - "", - ) + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + client.dealers.with_raw_response.delete( + "", + ) class TestAsyncDealers: @@ -234,20 +209,17 @@ class TestAsyncDealers: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = await async_client.dealers.retrieve( - "dealer_id", - ) - + dealer = await async_client.dealers.retrieve( + "dealer_id", + ) assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.dealers.with_raw_response.retrieve( - "dealer_id", - ) + response = await async_client.dealers.with_raw_response.retrieve( + "dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -257,64 +229,57 @@ async def test_raw_response_retrieve(self, async_client: AsyncPartnermax) -> Non @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.dealers.with_streaming_response.retrieve( - "dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + async with async_client.dealers.with_streaming_response.retrieve( + "dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = await response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) + dealer = await response.parse() + assert_matches_type(DealerDetail, dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_retrieve(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - await async_client.dealers.with_raw_response.retrieve( - "", - ) + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.with_raw_response.retrieve( + "", + ) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = await async_client.dealers.update( - dealer_id="dealer_id", - ) - + dealer = await async_client.dealers.update( + dealer_id="dealer_id", + ) assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_update_with_all_params(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = await async_client.dealers.update( - dealer_id="dealer_id", - address="xx", - business_name="xx", - city="xx", - contact_email="dev@stainless.com", - contact_phone="xxxxx", - metadata={"foo": "string"}, - postal_code="21029", - province_code="SE", - status="active", - idempotency_key="Idempotency-Key", - ) - + dealer = await async_client.dealers.update( + dealer_id="dealer_id", + address="xx", + business_name="xx", + city="xx", + contact_email="dev@stainless.com", + contact_phone="xxxxx", + metadata={"foo": "string"}, + postal_code="21029", + province_code="SE", + status="active", + idempotency_key="Idempotency-Key", + ) assert_matches_type(DealerDetail, dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_update(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.dealers.with_raw_response.update( - dealer_id="dealer_id", - ) + response = await async_client.dealers.with_raw_response.update( + dealer_id="dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -324,52 +289,45 @@ async def test_raw_response_update(self, async_client: AsyncPartnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_update(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.dealers.with_streaming_response.update( - dealer_id="dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + async with async_client.dealers.with_streaming_response.update( + dealer_id="dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = await response.parse() - assert_matches_type(DealerDetail, dealer, path=["response"]) + dealer = await response.parse() + assert_matches_type(DealerDetail, dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_update(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - await async_client.dealers.with_raw_response.update( - dealer_id="", - ) + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.with_raw_response.update( + dealer_id="", + ) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = await async_client.dealers.list() - + dealer = await async_client.dealers.list() assert_matches_type(AsyncCursorPage[DealerSummary], dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_with_all_params(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = await async_client.dealers.list( - cursor="cursor", - limit=1, - status="active", - ) - + dealer = await async_client.dealers.list( + cursor="cursor", + limit=1, + status="active", + ) assert_matches_type(AsyncCursorPage[DealerSummary], dealer, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.dealers.with_raw_response.list() + response = await async_client.dealers.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -379,33 +337,29 @@ async def test_raw_response_list(self, async_client: AsyncPartnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.dealers.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + async with async_client.dealers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = await response.parse() - assert_matches_type(AsyncCursorPage[DealerSummary], dealer, path=["response"]) + dealer = await response.parse() + assert_matches_type(AsyncCursorPage[DealerSummary], dealer, path=["response"]) assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_delete(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - dealer = await async_client.dealers.delete( - "dealer_id", - ) - + dealer = await async_client.dealers.delete( + "dealer_id", + ) assert dealer is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_delete(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - response = await async_client.dealers.with_raw_response.delete( - "dealer_id", - ) + response = await async_client.dealers.with_raw_response.delete( + "dealer_id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -415,23 +369,21 @@ async def test_raw_response_delete(self, async_client: AsyncPartnermax) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_delete(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - async with async_client.dealers.with_streaming_response.delete( - "dealer_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + async with async_client.dealers.with_streaming_response.delete( + "dealer_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - dealer = await response.parse() - assert dealer is None + dealer = await response.parse() + assert dealer is None assert cast(Any, response.is_closed) is True @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_delete(self, async_client: AsyncPartnermax) -> None: - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): - await async_client.dealers.with_raw_response.delete( - "", - ) + with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"): + await async_client.dealers.with_raw_response.delete( + "", + ) From efc630962361b39f78f40008dc883bb7c9a33b98 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:05:35 +0000 Subject: [PATCH 06/10] chore: update SDK settings --- .github/workflows/publish-pypi.yml | 5 ++--- .github/workflows/release-doctor.yml | 2 ++ .stats.yml | 4 ++-- bin/check-release-environment | 4 ++++ bin/publish-pypi | 6 +----- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index e6ffc7d..6e182f9 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -12,9 +12,6 @@ jobs: publish: name: publish runs-on: ubuntu-latest - permissions: - contents: read - id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -27,3 +24,5 @@ jobs: - name: Publish to PyPI run: | bash ./bin/publish-pypi + env: + PYPI_TOKEN: ${{ secrets.PARTNERMAX_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index fa7b46d..e57a1af 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -17,3 +17,5 @@ jobs: - name: Check release environment run: | bash ./bin/check-release-environment + env: + PYPI_TOKEN: ${{ secrets.PARTNERMAX_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.stats.yml b/.stats.yml index d02b966..ac0eadd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-fa55dc70ae5286c71a0faea047e23dfbb93be0bbfa3d7a0776d06c12a376444a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-5ae975b62d903965aae022a0b311cb7782b8413bb8651a45fdf7abc7c7661293.yml openapi_spec_hash: 841e984b131bfb9b83d26fdd9dbd5303 -config_hash: 410d10ad3652f4fdf97d2bd563de0d64 +config_hash: 07e7b101efb91dec2dcdcffb09ad3c63 diff --git a/bin/check-release-environment b/bin/check-release-environment index 1e951e9..b845b0f 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -2,6 +2,10 @@ errors=() +if [ -z "${PYPI_TOKEN}" ]; then + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") +fi + lenErrors=${#errors[@]} if [[ lenErrors -gt 0 ]]; then diff --git a/bin/publish-pypi b/bin/publish-pypi index 5895700..e72ca2f 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -4,8 +4,4 @@ set -eux rm -rf dist mkdir -p dist uv build -if [ -n "${PYPI_TOKEN:-}" ]; then - uv publish --token=$PYPI_TOKEN -else - uv publish -fi +uv publish --token=$PYPI_TOKEN From 957f874069a07618175ba519778cf850071b6536 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:05:53 +0000 Subject: [PATCH 07/10] chore: update SDK settings --- .github/workflows/publish-pypi.yml | 5 +++-- .github/workflows/release-doctor.yml | 2 -- .stats.yml | 4 ++-- bin/check-release-environment | 4 ---- bin/publish-pypi | 6 +++++- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 6e182f9..e6ffc7d 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -12,6 +12,9 @@ jobs: publish: name: publish runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -24,5 +27,3 @@ jobs: - name: Publish to PyPI run: | bash ./bin/publish-pypi - env: - PYPI_TOKEN: ${{ secrets.PARTNERMAX_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index e57a1af..fa7b46d 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -17,5 +17,3 @@ jobs: - name: Check release environment run: | bash ./bin/check-release-environment - env: - PYPI_TOKEN: ${{ secrets.PARTNERMAX_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.stats.yml b/.stats.yml index ac0eadd..d02b966 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-5ae975b62d903965aae022a0b311cb7782b8413bb8651a45fdf7abc7c7661293.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-fa55dc70ae5286c71a0faea047e23dfbb93be0bbfa3d7a0776d06c12a376444a.yml openapi_spec_hash: 841e984b131bfb9b83d26fdd9dbd5303 -config_hash: 07e7b101efb91dec2dcdcffb09ad3c63 +config_hash: 410d10ad3652f4fdf97d2bd563de0d64 diff --git a/bin/check-release-environment b/bin/check-release-environment index b845b0f..1e951e9 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -2,10 +2,6 @@ errors=() -if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") -fi - lenErrors=${#errors[@]} if [[ lenErrors -gt 0 ]]; then diff --git a/bin/publish-pypi b/bin/publish-pypi index e72ca2f..5895700 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -4,4 +4,8 @@ set -eux rm -rf dist mkdir -p dist uv build -uv publish --token=$PYPI_TOKEN +if [ -n "${PYPI_TOKEN:-}" ]; then + uv publish --token=$PYPI_TOKEN +else + uv publish +fi From 24bc9e86c88cecc8d74cb70b163452afa7b50f44 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:06:12 +0000 Subject: [PATCH 08/10] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d02b966..2033aec 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-fa55dc70ae5286c71a0faea047e23dfbb93be0bbfa3d7a0776d06c12a376444a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-551de06f2e3943c8d22bbcd2b71080e713a3cf667d2215055f07071e0591b786.yml openapi_spec_hash: 841e984b131bfb9b83d26fdd9dbd5303 -config_hash: 410d10ad3652f4fdf97d2bd563de0d64 +config_hash: fdfbf047967c33f75ad8c2e00d00218e From 4856e70d966e39d9421e6277c624e8eb4e6a1e38 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:06:29 +0000 Subject: [PATCH 09/10] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2033aec..d02b966 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-551de06f2e3943c8d22bbcd2b71080e713a3cf667d2215055f07071e0591b786.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-fa55dc70ae5286c71a0faea047e23dfbb93be0bbfa3d7a0776d06c12a376444a.yml openapi_spec_hash: 841e984b131bfb9b83d26fdd9dbd5303 -config_hash: fdfbf047967c33f75ad8c2e00d00218e +config_hash: 410d10ad3652f4fdf97d2bd563de0d64 From ab17f4ddc49c4a6a047cf3c0ee4ae83f60a35444 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:06:46 +0000 Subject: [PATCH 10/10] release: 0.11.1 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 15 +++++++++++++++ pyproject.toml | 2 +- src/partnermax/_version.py | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f7014c3..e82003f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.11.0" + ".": "0.11.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a72ecb6..d404d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.11.1 (2026-06-27) + +Full Changelog: [v0.11.0...v0.11.1](https://github.com/DealerMax-app/partnermax-python/compare/v0.11.0...v0.11.1) + +### Bug Fixes + +* regenerate SDK for PartnerMAX contract 1.5.3 ([ed19bab](https://github.com/DealerMax-app/partnermax-python/commit/ed19bab778dbe4c5dc148b9d1c4ee22ffde38308)) +* regenerate SDK with active dealer methods ([62bdc98](https://github.com/DealerMax-app/partnermax-python/commit/62bdc98e17d9222d23199332da41a40cc78f4dfc)) + + +### Chores + +* update SDK settings ([957f874](https://github.com/DealerMax-app/partnermax-python/commit/957f874069a07618175ba519778cf850071b6536)) +* update SDK settings ([efc6309](https://github.com/DealerMax-app/partnermax-python/commit/efc630962361b39f78f40008dc883bb7c9a33b98)) + ## 0.11.0 (2026-06-26) Full Changelog: [v0.10.1...v0.11.0](https://github.com/DealerMax-app/partnermax-python/compare/v0.10.1...v0.11.0) diff --git a/pyproject.toml b/pyproject.toml index 43f5f1c..4039f5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "partnermax" -version = "0.11.0" +version = "0.11.1" description = "The official Python library for the partnermax API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/partnermax/_version.py b/src/partnermax/_version.py index 3bafd55..c1ce69d 100644 --- a/src/partnermax/_version.py +++ b/src/partnermax/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "partnermax" -__version__ = "0.11.0" # x-release-please-version +__version__ = "0.11.1" # x-release-please-version