From cc0d0ba3fd3442403380309dd9cffc37e960d1b7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 14:13:41 +0000
Subject: [PATCH 1/2] feat(vehicles): used-vehicle CRUD + bulk insert for
v1.1.0
---
.stats.yml | 8 +-
README.md | 19 +
api.md | 26 +-
src/partnermax/_files.py | 2 +-
src/partnermax/resources/dealers/dealers.py | 16 +-
.../resources/dealers/vehicles/__init__.py | 33 ++
.../resources/dealers/vehicles/images.py | 453 ++++++++++++++++++
.../dealers/{ => vehicles}/vehicles.py | 76 ++-
.../types/dealers/vehicles/__init__.py | 7 +
.../dealers/vehicles/image_create_params.py | 19 +
.../types/dealers/vehicles/vehicle_image.py | 50 ++
.../dealers/vehicles/vehicle_image_list.py | 19 +
.../dealers/vehicles/__init__.py | 1 +
.../dealers/vehicles/test_images.py | 370 ++++++++++++++
14 files changed, 1070 insertions(+), 29 deletions(-)
create mode 100644 src/partnermax/resources/dealers/vehicles/__init__.py
create mode 100644 src/partnermax/resources/dealers/vehicles/images.py
rename src/partnermax/resources/dealers/{ => vehicles}/vehicles.py (92%)
create mode 100644 src/partnermax/types/dealers/vehicles/__init__.py
create mode 100644 src/partnermax/types/dealers/vehicles/image_create_params.py
create mode 100644 src/partnermax/types/dealers/vehicles/vehicle_image.py
create mode 100644 src/partnermax/types/dealers/vehicles/vehicle_image_list.py
create mode 100644 tests/api_resources/dealers/vehicles/__init__.py
create mode 100644 tests/api_resources/dealers/vehicles/test_images.py
diff --git a/.stats.yml b/.stats.yml
index 26bad1a..9de5f99 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 18
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-2efc95334578123c6faee89c66575c440aec573d479dd6b6e0ce0d6477472e23.yml
-openapi_spec_hash: c97c1474daf815592ca1a3a9fd29e482
-config_hash: ef989e9f9b2271740f1b742f1ab804af
+configured_endpoints: 21
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-5a0409bb0fddcda8df26a189afdb3328c0402ce1385e56c4793e2969efd2b290.yml
+openapi_spec_hash: fa78b80fb8127ab4e7761cbf9ccbbdd2
+config_hash: 84dd1a86856c12a806c07e9e23badbfd
diff --git a/README.md b/README.md
index 13465f4..fd2192a 100644
--- a/README.md
+++ b/README.md
@@ -170,6 +170,25 @@ nlt_settings = client.dealers.nlt_settings.update(
print(nlt_settings.down_payment_tiers)
```
+## File uploads
+
+Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`.
+
+```python
+from pathlib import Path
+from partnermax import Partnermax
+
+client = Partnermax()
+
+client.dealers.vehicles.images.create(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ file=Path("/path/to/file"),
+)
+```
+
+The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically.
+
## Handling errors
When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `partnermax.APIConnectionError` is raised.
diff --git a/api.md b/api.md
index bca3acb..cb3ea5d 100644
--- a/api.md
+++ b/api.md
@@ -72,9 +72,23 @@ from partnermax.types.dealers import (
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.delete(vehicle_id, \*, dealer_id) -> None
-- client.dealers.vehicles.bulk(dealer_id, \*\*params) -> BulkCreateVehiclesResponse
+- 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.delete(vehicle_id, \*, dealer_id) -> None
+- client.dealers.vehicles.bulk(dealer_id, \*\*params) -> BulkCreateVehiclesResponse
+
+### Images
+
+Types:
+
+```python
+from partnermax.types.dealers.vehicles import VehicleImage, VehicleImageList
+```
+
+Methods:
+
+- client.dealers.vehicles.images.create(vehicle_id, \*, dealer_id, \*\*params) -> VehicleImage
+- client.dealers.vehicles.images.list(vehicle_id, \*, dealer_id) -> VehicleImageList
+- client.dealers.vehicles.images.delete(image_id, \*, dealer_id, vehicle_id) -> None
diff --git a/src/partnermax/_files.py b/src/partnermax/_files.py
index 76da9e0..d962650 100644
--- a/src/partnermax/_files.py
+++ b/src/partnermax/_files.py
@@ -36,7 +36,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None:
if not is_file_content(obj):
prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`"
raise RuntimeError(
- f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead."
+ f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/DealerMax-app/partnermax-python/tree/main#file-uploads"
) from None
diff --git a/src/partnermax/resources/dealers/dealers.py b/src/partnermax/resources/dealers/dealers.py
index bb4b7da..3df5d55 100644
--- a/src/partnermax/resources/dealers/dealers.py
+++ b/src/partnermax/resources/dealers/dealers.py
@@ -18,14 +18,6 @@
)
from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform
-from .vehicles import (
- VehiclesResource,
- AsyncVehiclesResource,
- VehiclesResourceWithRawResponse,
- AsyncVehiclesResourceWithRawResponse,
- VehiclesResourceWithStreamingResponse,
- AsyncVehiclesResourceWithStreamingResponse,
-)
from ..._compat import cached_property
from ..._resource import SyncAPIResource, AsyncAPIResource
from ..._response import (
@@ -43,6 +35,14 @@
AsyncNltSettingsResourceWithStreamingResponse,
)
from ..._base_client import make_request_options
+from .vehicles.vehicles import (
+ VehiclesResource,
+ AsyncVehiclesResource,
+ VehiclesResourceWithRawResponse,
+ AsyncVehiclesResourceWithRawResponse,
+ VehiclesResourceWithStreamingResponse,
+ AsyncVehiclesResourceWithStreamingResponse,
+)
from ...types.dealer_detail import DealerDetail
from ...types.dealer_list_response import DealerListResponse
diff --git a/src/partnermax/resources/dealers/vehicles/__init__.py b/src/partnermax/resources/dealers/vehicles/__init__.py
new file mode 100644
index 0000000..d80c5c5
--- /dev/null
+++ b/src/partnermax/resources/dealers/vehicles/__init__.py
@@ -0,0 +1,33 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .images import (
+ ImagesResource,
+ AsyncImagesResource,
+ ImagesResourceWithRawResponse,
+ AsyncImagesResourceWithRawResponse,
+ ImagesResourceWithStreamingResponse,
+ AsyncImagesResourceWithStreamingResponse,
+)
+from .vehicles import (
+ VehiclesResource,
+ AsyncVehiclesResource,
+ VehiclesResourceWithRawResponse,
+ AsyncVehiclesResourceWithRawResponse,
+ VehiclesResourceWithStreamingResponse,
+ AsyncVehiclesResourceWithStreamingResponse,
+)
+
+__all__ = [
+ "ImagesResource",
+ "AsyncImagesResource",
+ "ImagesResourceWithRawResponse",
+ "AsyncImagesResourceWithRawResponse",
+ "ImagesResourceWithStreamingResponse",
+ "AsyncImagesResourceWithStreamingResponse",
+ "VehiclesResource",
+ "AsyncVehiclesResource",
+ "VehiclesResourceWithRawResponse",
+ "AsyncVehiclesResourceWithRawResponse",
+ "VehiclesResourceWithStreamingResponse",
+ "AsyncVehiclesResourceWithStreamingResponse",
+]
diff --git a/src/partnermax/resources/dealers/vehicles/images.py b/src/partnermax/resources/dealers/vehicles/images.py
new file mode 100644
index 0000000..b0a30f4
--- /dev/null
+++ b/src/partnermax/resources/dealers/vehicles/images.py
@@ -0,0 +1,453 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Mapping, cast
+
+import httpx
+
+from ...._files import deepcopy_with_paths
+from ...._types import Body, Query, Headers, NoneType, NotGiven, FileTypes, not_given
+from ...._utils import extract_files, path_template, maybe_transform, async_maybe_transform
+from ...._compat import cached_property
+from ...._resource import SyncAPIResource, AsyncAPIResource
+from ...._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from ...._base_client import make_request_options
+from ....types.dealers.vehicles import image_create_params
+from ....types.dealers.vehicles.vehicle_image import VehicleImage
+from ....types.dealers.vehicles.vehicle_image_list import VehicleImageList
+
+__all__ = ["ImagesResource", "AsyncImagesResource"]
+
+
+class ImagesResource(SyncAPIResource):
+ """Used-vehicle stock management for partner-owned dealers.
+
+ The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer).
+ """
+
+ @cached_property
+ def with_raw_response(self) -> ImagesResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/DealerMax-app/partnermax-python#accessing-raw-response-data-eg-headers
+ """
+ return ImagesResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ImagesResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/DealerMax-app/partnermax-python#with_streaming_response
+ """
+ return ImagesResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ vehicle_id: str,
+ *,
+ dealer_id: str,
+ file: FileTypes,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VehicleImage:
+ """
+ Attach a photo to a used vehicle.
+
+ The partner posts photos one at a time, in the desired display order. The first
+ photo becomes the cover (`position=1`, `is_cover=true`) automatically;
+ subsequent photos get the next `position`. There is intentionally no separate
+ "set cover" endpoint — order is the contract. To re-order, DELETE and re-POST.
+
+ Up to `20` photos per vehicle. Bigger payloads return `413`; unsupported formats
+ return `415`; missing storage credentials return `503 storage_not_configured`.
+
+ The uploaded photo is visible on every AI surface (MCP `search_vehicles`, Custom
+ GPT `search-vehicles-network`, the dealer site SEO/JSON-LD, NLWeb `/ask`) within
+ the next apimax cache TTL (≤ 5 min).
+
+ Args:
+ file: The photo file. JPEG, PNG, or WebP, up to 15 MB. WebP is converted to PNG
+ server-side.
+
+ 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
+ """
+ if not dealer_id:
+ raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}")
+ if not vehicle_id:
+ raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}")
+ body = deepcopy_with_paths({"file": file}, [["file"]])
+ files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ return self._post(
+ path_template(
+ "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images", dealer_id=dealer_id, vehicle_id=vehicle_id
+ ),
+ body=maybe_transform(body, image_create_params.ImageCreateParams),
+ files=files,
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=VehicleImage,
+ )
+
+ def list(
+ self,
+ vehicle_id: str,
+ *,
+ dealer_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VehicleImageList:
+ """
+ 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.
+
+ 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
+ """
+ if not dealer_id:
+ raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}")
+ if not vehicle_id:
+ raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}")
+ return self._get(
+ path_template(
+ "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images", dealer_id=dealer_id, vehicle_id=vehicle_id
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=VehicleImageList,
+ )
+
+ def delete(
+ self,
+ image_id: str,
+ *,
+ dealer_id: str,
+ vehicle_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Remove a photo from a vehicle.
+
+ If the deleted photo was the cover (`position=1`), the next photo in order is
+ promoted to cover automatically — partnermax re-ranks every remaining photo to
+ contiguous 1..N so the partner never has to reason about gaps in the ordinal
+ sequence.
+
+ Returns `404 vehicle_image_not_found` if the image id is unknown or belongs to a
+ different vehicle (cross-partner enumeration is blocked by the dealer + vehicle
+ ACL chain).
+
+ 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
+ """
+ if not dealer_id:
+ raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}")
+ if not vehicle_id:
+ raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}")
+ if not image_id:
+ raise ValueError(f"Expected a non-empty value for `image_id` but received {image_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template(
+ "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images/{image_id}",
+ dealer_id=dealer_id,
+ vehicle_id=vehicle_id,
+ image_id=image_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class AsyncImagesResource(AsyncAPIResource):
+ """Used-vehicle stock management for partner-owned dealers.
+
+ The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer).
+ """
+
+ @cached_property
+ def with_raw_response(self) -> AsyncImagesResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/DealerMax-app/partnermax-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncImagesResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncImagesResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/DealerMax-app/partnermax-python#with_streaming_response
+ """
+ return AsyncImagesResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ vehicle_id: str,
+ *,
+ dealer_id: str,
+ file: FileTypes,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VehicleImage:
+ """
+ Attach a photo to a used vehicle.
+
+ The partner posts photos one at a time, in the desired display order. The first
+ photo becomes the cover (`position=1`, `is_cover=true`) automatically;
+ subsequent photos get the next `position`. There is intentionally no separate
+ "set cover" endpoint — order is the contract. To re-order, DELETE and re-POST.
+
+ Up to `20` photos per vehicle. Bigger payloads return `413`; unsupported formats
+ return `415`; missing storage credentials return `503 storage_not_configured`.
+
+ The uploaded photo is visible on every AI surface (MCP `search_vehicles`, Custom
+ GPT `search-vehicles-network`, the dealer site SEO/JSON-LD, NLWeb `/ask`) within
+ the next apimax cache TTL (≤ 5 min).
+
+ Args:
+ file: The photo file. JPEG, PNG, or WebP, up to 15 MB. WebP is converted to PNG
+ server-side.
+
+ 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
+ """
+ if not dealer_id:
+ raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}")
+ if not vehicle_id:
+ raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}")
+ body = deepcopy_with_paths({"file": file}, [["file"]])
+ files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
+ # It should be noted that the actual Content-Type header that will be
+ # sent to the server will contain a `boundary` parameter, e.g.
+ # multipart/form-data; boundary=---abc--
+ extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
+ return await self._post(
+ path_template(
+ "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images", dealer_id=dealer_id, vehicle_id=vehicle_id
+ ),
+ body=await async_maybe_transform(body, image_create_params.ImageCreateParams),
+ files=files,
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=VehicleImage,
+ )
+
+ async def list(
+ self,
+ vehicle_id: str,
+ *,
+ dealer_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> VehicleImageList:
+ """
+ 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.
+
+ 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
+ """
+ if not dealer_id:
+ raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}")
+ if not vehicle_id:
+ raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}")
+ return await self._get(
+ path_template(
+ "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images", dealer_id=dealer_id, vehicle_id=vehicle_id
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=VehicleImageList,
+ )
+
+ async def delete(
+ self,
+ image_id: str,
+ *,
+ dealer_id: str,
+ vehicle_id: str,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Remove a photo from a vehicle.
+
+ If the deleted photo was the cover (`position=1`), the next photo in order is
+ promoted to cover automatically — partnermax re-ranks every remaining photo to
+ contiguous 1..N so the partner never has to reason about gaps in the ordinal
+ sequence.
+
+ Returns `404 vehicle_image_not_found` if the image id is unknown or belongs to a
+ different vehicle (cross-partner enumeration is blocked by the dealer + vehicle
+ ACL chain).
+
+ 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
+ """
+ if not dealer_id:
+ raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}")
+ if not vehicle_id:
+ raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}")
+ if not image_id:
+ raise ValueError(f"Expected a non-empty value for `image_id` but received {image_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template(
+ "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images/{image_id}",
+ dealer_id=dealer_id,
+ vehicle_id=vehicle_id,
+ image_id=image_id,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class ImagesResourceWithRawResponse:
+ def __init__(self, images: ImagesResource) -> None:
+ self._images = images
+
+ self.create = to_raw_response_wrapper(
+ images.create,
+ )
+ self.list = to_raw_response_wrapper(
+ images.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ images.delete,
+ )
+
+
+class AsyncImagesResourceWithRawResponse:
+ def __init__(self, images: AsyncImagesResource) -> None:
+ self._images = images
+
+ self.create = async_to_raw_response_wrapper(
+ images.create,
+ )
+ self.list = async_to_raw_response_wrapper(
+ images.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ images.delete,
+ )
+
+
+class ImagesResourceWithStreamingResponse:
+ def __init__(self, images: ImagesResource) -> None:
+ self._images = images
+
+ self.create = to_streamed_response_wrapper(
+ images.create,
+ )
+ self.list = to_streamed_response_wrapper(
+ images.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ images.delete,
+ )
+
+
+class AsyncImagesResourceWithStreamingResponse:
+ def __init__(self, images: AsyncImagesResource) -> None:
+ self._images = images
+
+ self.create = async_to_streamed_response_wrapper(
+ images.create,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ images.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ images.delete,
+ )
diff --git a/src/partnermax/resources/dealers/vehicles.py b/src/partnermax/resources/dealers/vehicles/vehicles.py
similarity index 92%
rename from src/partnermax/resources/dealers/vehicles.py
rename to src/partnermax/resources/dealers/vehicles/vehicles.py
index 99332b1..f0c5a58 100644
--- a/src/partnermax/resources/dealers/vehicles.py
+++ b/src/partnermax/resources/dealers/vehicles/vehicles.py
@@ -7,27 +7,35 @@
import httpx
-from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
-from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform
-from ..._compat import cached_property
-from ..._resource import SyncAPIResource, AsyncAPIResource
-from ..._response import (
+from .images import (
+ ImagesResource,
+ AsyncImagesResource,
+ ImagesResourceWithRawResponse,
+ AsyncImagesResourceWithRawResponse,
+ ImagesResourceWithStreamingResponse,
+ AsyncImagesResourceWithStreamingResponse,
+)
+from ...._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given
+from ...._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform
+from ...._compat import cached_property
+from ...._resource import SyncAPIResource, AsyncAPIResource
+from ...._response import (
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ..._base_client import make_request_options
-from ...types.dealers import (
+from ...._base_client import make_request_options
+from ....types.dealers import (
vehicle_bulk_params,
vehicle_list_params,
vehicle_create_params,
vehicle_update_params,
vehicle_retrieve_params,
)
-from ...types.dealers.vehicle_list import VehicleList
-from ...types.dealers.vehicle_detail import VehicleDetail
-from ...types.dealers.bulk_create_vehicles_response import BulkCreateVehiclesResponse
+from ....types.dealers.vehicle_list import VehicleList
+from ....types.dealers.vehicle_detail import VehicleDetail
+from ....types.dealers.bulk_create_vehicles_response import BulkCreateVehiclesResponse
__all__ = ["VehiclesResource", "AsyncVehiclesResource"]
@@ -38,6 +46,14 @@ class VehiclesResource(SyncAPIResource):
The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer).
"""
+ @cached_property
+ def images(self) -> ImagesResource:
+ """Used-vehicle stock management for partner-owned dealers.
+
+ The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer).
+ """
+ return ImagesResource(self._client)
+
@cached_property
def with_raw_response(self) -> VehiclesResourceWithRawResponse:
"""
@@ -514,6 +530,14 @@ class AsyncVehiclesResource(AsyncAPIResource):
The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer).
"""
+ @cached_property
+ def images(self) -> AsyncImagesResource:
+ """Used-vehicle stock management for partner-owned dealers.
+
+ The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer).
+ """
+ return AsyncImagesResource(self._client)
+
@cached_property
def with_raw_response(self) -> AsyncVehiclesResourceWithRawResponse:
"""
@@ -1007,6 +1031,14 @@ def __init__(self, vehicles: VehiclesResource) -> None:
vehicles.bulk,
)
+ @cached_property
+ def images(self) -> ImagesResourceWithRawResponse:
+ """Used-vehicle stock management for partner-owned dealers.
+
+ The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer).
+ """
+ return ImagesResourceWithRawResponse(self._vehicles.images)
+
class AsyncVehiclesResourceWithRawResponse:
def __init__(self, vehicles: AsyncVehiclesResource) -> None:
@@ -1031,6 +1063,14 @@ def __init__(self, vehicles: AsyncVehiclesResource) -> None:
vehicles.bulk,
)
+ @cached_property
+ def images(self) -> AsyncImagesResourceWithRawResponse:
+ """Used-vehicle stock management for partner-owned dealers.
+
+ The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer).
+ """
+ return AsyncImagesResourceWithRawResponse(self._vehicles.images)
+
class VehiclesResourceWithStreamingResponse:
def __init__(self, vehicles: VehiclesResource) -> None:
@@ -1055,6 +1095,14 @@ def __init__(self, vehicles: VehiclesResource) -> None:
vehicles.bulk,
)
+ @cached_property
+ def images(self) -> ImagesResourceWithStreamingResponse:
+ """Used-vehicle stock management for partner-owned dealers.
+
+ The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer).
+ """
+ return ImagesResourceWithStreamingResponse(self._vehicles.images)
+
class AsyncVehiclesResourceWithStreamingResponse:
def __init__(self, vehicles: AsyncVehiclesResource) -> None:
@@ -1078,3 +1126,11 @@ def __init__(self, vehicles: AsyncVehiclesResource) -> None:
self.bulk = async_to_streamed_response_wrapper(
vehicles.bulk,
)
+
+ @cached_property
+ def images(self) -> AsyncImagesResourceWithStreamingResponse:
+ """Used-vehicle stock management for partner-owned dealers.
+
+ The partner uploads each used vehicle by its canonical Motornet UNI code; DealerMAX joins the partner-provided pricing and stock metadata with the catalog master so the resulting listing is immediately indexed by the AI surfaces (MCP server, ChatGPT Custom GPT, NLWeb /ask, and the SEO/JSON-LD layer).
+ """
+ return AsyncImagesResourceWithStreamingResponse(self._vehicles.images)
diff --git a/src/partnermax/types/dealers/vehicles/__init__.py b/src/partnermax/types/dealers/vehicles/__init__.py
new file mode 100644
index 0000000..7eafcde
--- /dev/null
+++ b/src/partnermax/types/dealers/vehicles/__init__.py
@@ -0,0 +1,7 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from .vehicle_image import VehicleImage as VehicleImage
+from .vehicle_image_list import VehicleImageList as VehicleImageList
+from .image_create_params import ImageCreateParams as ImageCreateParams
diff --git a/src/partnermax/types/dealers/vehicles/image_create_params.py b/src/partnermax/types/dealers/vehicles/image_create_params.py
new file mode 100644
index 0000000..1f284d3
--- /dev/null
+++ b/src/partnermax/types/dealers/vehicles/image_create_params.py
@@ -0,0 +1,19 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Required, TypedDict
+
+from ...._types import FileTypes
+
+__all__ = ["ImageCreateParams"]
+
+
+class ImageCreateParams(TypedDict, total=False):
+ dealer_id: Required[str]
+
+ file: Required[FileTypes]
+ """The photo file.
+
+ JPEG, PNG, or WebP, up to 15 MB. WebP is converted to PNG server-side.
+ """
diff --git a/src/partnermax/types/dealers/vehicles/vehicle_image.py b/src/partnermax/types/dealers/vehicles/vehicle_image.py
new file mode 100644
index 0000000..b280f59
--- /dev/null
+++ b/src/partnermax/types/dealers/vehicles/vehicle_image.py
@@ -0,0 +1,50 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from datetime import datetime
+
+from ...._models import BaseModel
+
+__all__ = ["VehicleImage"]
+
+
+class VehicleImage(BaseModel):
+ """A single photo attached to a used vehicle.
+
+ Returned by ``POST .../images`` (single upload) and as elements of
+ ``VehicleImageList.data`` (list endpoint). ``position == 1`` is the
+ cover photo by convention; ``is_cover`` mirrors it for readability.
+
+ The ``image_url`` is the raw Supabase public URL. The cross-network
+ AI surfaces (MCP search_vehicles, Custom GPT, NLWeb) embed a CDN
+ AVIF-transformed variant; partners that render this URL themselves
+ can apply Supabase's render-service query parameters
+ (`?format=avif&quality=70&resize=contain`) for the same treatment.
+ """
+
+ created_at: datetime
+
+ image_id: str
+ """Opaque image identifier in the form `img_`.
+
+ Use as the path parameter on `DELETE` to remove the photo.
+ """
+
+ image_url: str
+ """Public Supabase Storage URL of the original upload.
+
+ Stable for the lifetime of the photo.
+ """
+
+ is_cover: bool
+ """True when `position == 1`.
+
+ Surfaced as a separate field so partners can branch on cover handling without
+ doing integer arithmetic on positions.
+ """
+
+ position: int
+ """1-based ordinal among this vehicle's photos.
+
+ `position=1` is the cover that appears on every AI surface; subsequent positions
+ are gallery photos shown on the dealer site detail page.
+ """
diff --git a/src/partnermax/types/dealers/vehicles/vehicle_image_list.py b/src/partnermax/types/dealers/vehicles/vehicle_image_list.py
new file mode 100644
index 0000000..1260c14
--- /dev/null
+++ b/src/partnermax/types/dealers/vehicles/vehicle_image_list.py
@@ -0,0 +1,19 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+
+from ...._models import BaseModel
+from .vehicle_image import VehicleImage
+
+__all__ = ["VehicleImageList"]
+
+
+class VehicleImageList(BaseModel):
+ """All photos attached to a single vehicle, ordered by ``position``.
+
+ No pagination — a vehicle is capped at
+ ``VEHICLE_IMAGE_MAX_PER_VEHICLE`` photos by contract, so the full list
+ always fits in one response.
+ """
+
+ data: List[VehicleImage]
diff --git a/tests/api_resources/dealers/vehicles/__init__.py b/tests/api_resources/dealers/vehicles/__init__.py
new file mode 100644
index 0000000..fd8019a
--- /dev/null
+++ b/tests/api_resources/dealers/vehicles/__init__.py
@@ -0,0 +1 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/dealers/vehicles/test_images.py b/tests/api_resources/dealers/vehicles/test_images.py
new file mode 100644
index 0000000..37084fb
--- /dev/null
+++ b/tests/api_resources/dealers/vehicles/test_images.py
@@ -0,0 +1,370 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from partnermax import Partnermax, AsyncPartnermax
+from tests.utils import assert_matches_type
+from partnermax.types.dealers.vehicles import VehicleImage, VehicleImageList
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestImages:
+ 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:
+ image = client.dealers.vehicles.images.create(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ file=b"Example data",
+ )
+ assert_matches_type(VehicleImage, image, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Partnermax) -> None:
+ response = client.dealers.vehicles.images.with_raw_response.create(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ file=b"Example data",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ image = response.parse()
+ assert_matches_type(VehicleImage, image, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Partnermax) -> None:
+ with client.dealers.vehicles.images.with_streaming_response.create(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ file=b"Example data",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ image = response.parse()
+ assert_matches_type(VehicleImage, image, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_create(self, client: Partnermax) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"):
+ client.dealers.vehicles.images.with_raw_response.create(
+ vehicle_id="vehicle_id",
+ dealer_id="",
+ file=b"Example data",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"):
+ client.dealers.vehicles.images.with_raw_response.create(
+ vehicle_id="",
+ dealer_id="dealer_id",
+ file=b"Example data",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Partnermax) -> None:
+ image = client.dealers.vehicles.images.list(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ )
+ assert_matches_type(VehicleImageList, image, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Partnermax) -> None:
+ response = client.dealers.vehicles.images.with_raw_response.list(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ image = response.parse()
+ assert_matches_type(VehicleImageList, image, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Partnermax) -> None:
+ with client.dealers.vehicles.images.with_streaming_response.list(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ image = response.parse()
+ assert_matches_type(VehicleImageList, image, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_list(self, client: Partnermax) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"):
+ client.dealers.vehicles.images.with_raw_response.list(
+ vehicle_id="vehicle_id",
+ dealer_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"):
+ client.dealers.vehicles.images.with_raw_response.list(
+ vehicle_id="",
+ dealer_id="dealer_id",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Partnermax) -> None:
+ image = client.dealers.vehicles.images.delete(
+ image_id="image_id",
+ dealer_id="dealer_id",
+ vehicle_id="vehicle_id",
+ )
+ assert image is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Partnermax) -> None:
+ response = client.dealers.vehicles.images.with_raw_response.delete(
+ image_id="image_id",
+ dealer_id="dealer_id",
+ vehicle_id="vehicle_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ image = response.parse()
+ assert image is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Partnermax) -> None:
+ with client.dealers.vehicles.images.with_streaming_response.delete(
+ image_id="image_id",
+ dealer_id="dealer_id",
+ vehicle_id="vehicle_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ image = response.parse()
+ assert image is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Partnermax) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"):
+ client.dealers.vehicles.images.with_raw_response.delete(
+ image_id="image_id",
+ dealer_id="",
+ vehicle_id="vehicle_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"):
+ client.dealers.vehicles.images.with_raw_response.delete(
+ image_id="image_id",
+ dealer_id="dealer_id",
+ vehicle_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `image_id` but received ''"):
+ client.dealers.vehicles.images.with_raw_response.delete(
+ image_id="",
+ dealer_id="dealer_id",
+ vehicle_id="vehicle_id",
+ )
+
+
+class TestAsyncImages:
+ parametrize = pytest.mark.parametrize(
+ "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:
+ image = await async_client.dealers.vehicles.images.create(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ file=b"Example data",
+ )
+ assert_matches_type(VehicleImage, image, 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.vehicles.images.with_raw_response.create(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ file=b"Example data",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ image = await response.parse()
+ assert_matches_type(VehicleImage, image, 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.vehicles.images.with_streaming_response.create(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ file=b"Example data",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ image = await response.parse()
+ assert_matches_type(VehicleImage, image, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_create(self, async_client: AsyncPartnermax) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"):
+ await async_client.dealers.vehicles.images.with_raw_response.create(
+ vehicle_id="vehicle_id",
+ dealer_id="",
+ file=b"Example data",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"):
+ await async_client.dealers.vehicles.images.with_raw_response.create(
+ vehicle_id="",
+ dealer_id="dealer_id",
+ file=b"Example data",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncPartnermax) -> None:
+ image = await async_client.dealers.vehicles.images.list(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ )
+ assert_matches_type(VehicleImageList, image, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncPartnermax) -> None:
+ response = await async_client.dealers.vehicles.images.with_raw_response.list(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ image = await response.parse()
+ assert_matches_type(VehicleImageList, image, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncPartnermax) -> None:
+ async with async_client.dealers.vehicles.images.with_streaming_response.list(
+ vehicle_id="vehicle_id",
+ dealer_id="dealer_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ image = await response.parse()
+ assert_matches_type(VehicleImageList, image, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_list(self, async_client: AsyncPartnermax) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"):
+ await async_client.dealers.vehicles.images.with_raw_response.list(
+ vehicle_id="vehicle_id",
+ dealer_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"):
+ await async_client.dealers.vehicles.images.with_raw_response.list(
+ vehicle_id="",
+ dealer_id="dealer_id",
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncPartnermax) -> None:
+ image = await async_client.dealers.vehicles.images.delete(
+ image_id="image_id",
+ dealer_id="dealer_id",
+ vehicle_id="vehicle_id",
+ )
+ assert image is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncPartnermax) -> None:
+ response = await async_client.dealers.vehicles.images.with_raw_response.delete(
+ image_id="image_id",
+ dealer_id="dealer_id",
+ vehicle_id="vehicle_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ image = await response.parse()
+ assert image is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncPartnermax) -> None:
+ async with async_client.dealers.vehicles.images.with_streaming_response.delete(
+ image_id="image_id",
+ dealer_id="dealer_id",
+ vehicle_id="vehicle_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ image = await response.parse()
+ assert image is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncPartnermax) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `dealer_id` but received ''"):
+ await async_client.dealers.vehicles.images.with_raw_response.delete(
+ image_id="image_id",
+ dealer_id="",
+ vehicle_id="vehicle_id",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"):
+ await async_client.dealers.vehicles.images.with_raw_response.delete(
+ image_id="image_id",
+ dealer_id="dealer_id",
+ vehicle_id="",
+ )
+
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `image_id` but received ''"):
+ await async_client.dealers.vehicles.images.with_raw_response.delete(
+ image_id="",
+ dealer_id="dealer_id",
+ vehicle_id="vehicle_id",
+ )
From d544eb05d9815a6baaa3af16cafc25701016fa65 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 14:14:26 +0000
Subject: [PATCH 2/2] release: 0.9.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 8 ++++++++
pyproject.toml | 2 +-
src/partnermax/_version.py | 2 +-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 6538ca9..6d78745 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.8.0"
+ ".": "0.9.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8af65ae..5412df1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.9.0 (2026-05-18)
+
+Full Changelog: [v0.8.0...v0.9.0](https://github.com/DealerMax-app/partnermax-python/compare/v0.8.0...v0.9.0)
+
+### Features
+
+* **vehicles:** used-vehicle CRUD + bulk insert for v1.1.0 ([cc0d0ba](https://github.com/DealerMax-app/partnermax-python/commit/cc0d0ba3fd3442403380309dd9cffc37e960d1b7))
+
## 0.8.0 (2026-05-18)
Full Changelog: [v0.7.0...v0.8.0](https://github.com/DealerMax-app/partnermax-python/compare/v0.7.0...v0.8.0)
diff --git a/pyproject.toml b/pyproject.toml
index 0f907fa..a1e3262 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "partnermax"
-version = "0.8.0"
+version = "0.9.0"
description = "The official Python library for the partnermax API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/partnermax/_version.py b/src/partnermax/_version.py
index 06afba4..f52be1e 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.8.0" # x-release-please-version
+__version__ = "0.9.0" # x-release-please-version