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/.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/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/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/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/_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/_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 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", + )