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/.stats.yml b/.stats.yml
index 02a2d07..d02b966 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_hash: 1c0790919f1143b6ec46ba0c811a86f4
-config_hash: 18c6c7df17bad3320b9f2c126947f00a
+configured_endpoints: 20
+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/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/README.md b/README.md
index 13465f4..1f2c679 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:
@@ -297,7 +351,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/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/_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/_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
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..3c4a720 100644
--- a/src/partnermax/resources/dealers/dealers.py
+++ b/src/partnermax/resources/dealers/dealers.py
@@ -7,7 +7,7 @@
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 +26,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
+from ...pagination import SyncCursorPage, AsyncCursorPage
from .nlt_settings import (
NltSettingsResource,
AsyncNltSettingsResource,
@@ -34,7 +35,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 +45,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 +88,6 @@ 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,
- )
-
def retrieve(
self,
dealer_id: str,
@@ -251,7 +193,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 +207,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,7 +224,7 @@ def list(
dealer_list_params.DealerListParams,
),
),
- cast_to=DealerListResponse,
+ model=DealerSummary,
)
def delete(
@@ -358,65 +301,6 @@ 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,
- )
-
async def retrieve(
self,
dealer_id: str,
@@ -510,7 +394,7 @@ async def update(
cast_to=DealerDetail,
)
- async def list(
+ def list(
self,
*,
cursor: Optional[str] | Omit = omit,
@@ -522,7 +406,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 +420,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,7 +437,7 @@ async def list(
dealer_list_params.DealerListParams,
),
),
- cast_to=DealerListResponse,
+ model=DealerSummary,
)
async def delete(
@@ -595,9 +480,6 @@ 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,
)
@@ -632,9 +514,6 @@ 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,
)
@@ -669,9 +548,6 @@ class DealersResourceWithStreamingResponse:
def __init__(self, dealers: DealersResource) -> None:
self._dealers = dealers
- self.create = to_streamed_response_wrapper(
- dealers.create,
- )
self.retrieve = to_streamed_response_wrapper(
dealers.retrieve,
)
@@ -706,9 +582,6 @@ 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,
)
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..ed6c489 100644
--- a/src/partnermax/resources/dealers/vehicles/images.py
+++ b/src/partnermax/resources/dealers/vehicles/images.py
@@ -122,8 +122,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
@@ -303,8 +304,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/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/test_dealers.py b/tests/api_resources/test_dealers.py
index dd5d3fa..b607716 100644
--- a/tests/api_resources/test_dealers.py
+++ b/tests/api_resources/test_dealers.py
@@ -9,10 +9,8 @@
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
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -20,83 +18,6 @@
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:
@@ -203,7 +124,7 @@ def test_path_params_update(self, client: Partnermax) -> None:
@parametrize
def test_method_list(self, client: Partnermax) -> None:
dealer = client.dealers.list()
- 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
@@ -213,7 +134,7 @@ def test_method_list_with_all_params(self, client: Partnermax) -> None:
limit=1,
status="active",
)
- 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
@@ -223,7 +144,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"
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
@@ -233,7 +154,7 @@ def test_streaming_response_list(self, client: Partnermax) -> None:
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"])
assert cast(Any, response.is_closed) is True
@@ -285,83 +206,6 @@ 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:
@@ -468,7 +312,7 @@ async def test_path_params_update(self, async_client: AsyncPartnermax) -> None:
@parametrize
async def test_method_list(self, async_client: AsyncPartnermax) -> None:
dealer = await async_client.dealers.list()
- 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
@@ -478,7 +322,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncPartnermax)
limit=1,
status="active",
)
- 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
@@ -488,7 +332,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"
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
@@ -498,7 +342,7 @@ async def test_streaming_response_list(self, async_client: AsyncPartnermax) -> N
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"])
assert cast(Any, response.is_closed) is True
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)