diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 6e182f9..e6ffc7d 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -12,6 +12,9 @@ jobs: publish: name: publish runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -24,5 +27,3 @@ jobs: - name: Publish to PyPI run: | bash ./bin/publish-pypi - env: - PYPI_TOKEN: ${{ secrets.PARTNERMAX_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index e57a1af..fa7b46d 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -17,5 +17,3 @@ jobs: - name: Check release environment run: | bash ./bin/check-release-environment - env: - PYPI_TOKEN: ${{ secrets.PARTNERMAX_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a713055..9d5158b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.12.0" + ".": "0.12.1" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index dc46d65..47c64e3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-e9423fadcdede8fbb63a47e273c68c8941e6bff3f2d61257d31fe9a1a1a7f72b.yml -openapi_spec_hash: 841e984b131bfb9b83d26fdd9dbd5303 -config_hash: 89f4c4e21186c377826cbe4a5685df09 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/azure/partnermax-0c2a3d56340127842c8ff928074b12d81705700b06b497d6965138738b28b656.yml +openapi_spec_hash: 515afa9ff78787ae666a539d452d5d35 +config_hash: a9bb2c41f2e0cabac79eda1df90a1445 diff --git a/CHANGELOG.md b/CHANGELOG.md index c630831..2762c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.12.1 (2026-06-28) + +Full Changelog: [v0.12.0...v0.12.1](https://github.com/DealerMax-app/partnermax-python/compare/v0.12.0...v0.12.1) + +### Bug Fixes + +* regenerate SDK from openapi 1.5.10 upload schema ([05c5f4e](https://github.com/DealerMax-app/partnermax-python/commit/05c5f4e18c753cbed8f1de91918301aa161778b9)) + ## 0.12.0 (2026-06-28) Full Changelog: [v0.11.1...v0.12.0](https://github.com/DealerMax-app/partnermax-python/compare/v0.11.1...v0.12.0) diff --git a/README.md b/README.md index 1f2c679..4051510 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,25 @@ nlt_settings = client.dealers.nlt_settings.update( print(nlt_settings.down_payment_tiers) ``` +## File uploads + +Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. + +```python +from pathlib import Path +from partnermax import Partnermax + +client = Partnermax() + +client.dealers.vehicles.images.create( + vehicle_id="vehicle_id", + dealer_id="dealer_id", + file=Path("/path/to/file"), +) +``` + +The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `partnermax.APIConnectionError` is raised. diff --git a/bin/check-release-environment b/bin/check-release-environment index b845b0f..1e951e9 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -2,10 +2,6 @@ errors=() -if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") -fi - lenErrors=${#errors[@]} if [[ lenErrors -gt 0 ]]; then diff --git a/bin/publish-pypi b/bin/publish-pypi index e72ca2f..5895700 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -4,4 +4,8 @@ set -eux rm -rf dist mkdir -p dist uv build -uv publish --token=$PYPI_TOKEN +if [ -n "${PYPI_TOKEN:-}" ]; then + uv publish --token=$PYPI_TOKEN +else + uv publish +fi diff --git a/pyproject.toml b/pyproject.toml index 67803cd..1d60733 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "partnermax" -version = "0.12.0" +version = "0.12.1" 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 a7850bc..02ab6f0 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.12.0" # x-release-please-version +__version__ = "0.12.1" # x-release-please-version diff --git a/src/partnermax/resources/dealers/vehicles/images.py b/src/partnermax/resources/dealers/vehicles/images.py index ed6c489..b2aa56b 100644 --- a/src/partnermax/resources/dealers/vehicles/images.py +++ b/src/partnermax/resources/dealers/vehicles/images.py @@ -2,10 +2,13 @@ from __future__ import annotations +from typing import Mapping, cast + import httpx -from ...._types import Body, Query, Headers, NoneType, NotGiven, not_given -from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._files import deepcopy_with_paths +from ...._types import Body, Query, Headers, NoneType, NotGiven, FileTypes, not_given +from ...._utils import extract_files, path_template, maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import ( @@ -52,7 +55,7 @@ def create( vehicle_id: str, *, dealer_id: str, - file: str, + file: FileTypes, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -91,6 +94,8 @@ def create( raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") if not vehicle_id: raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") + body = deepcopy_with_paths({"file": file}, [["file"]]) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- @@ -99,7 +104,8 @@ def create( path_template( "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images", dealer_id=dealer_id, vehicle_id=vehicle_id ), - body=maybe_transform({"file": file}, image_create_params.ImageCreateParams), + body=maybe_transform(body, image_create_params.ImageCreateParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -234,7 +240,7 @@ async def create( vehicle_id: str, *, dealer_id: str, - file: str, + file: FileTypes, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -273,6 +279,8 @@ async def create( raise ValueError(f"Expected a non-empty value for `dealer_id` but received {dealer_id!r}") if not vehicle_id: raise ValueError(f"Expected a non-empty value for `vehicle_id` but received {vehicle_id!r}") + body = deepcopy_with_paths({"file": file}, [["file"]]) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- @@ -281,7 +289,8 @@ async def create( path_template( "/v1/dealers/{dealer_id}/vehicles/{vehicle_id}/images", dealer_id=dealer_id, vehicle_id=vehicle_id ), - body=await async_maybe_transform({"file": file}, image_create_params.ImageCreateParams), + body=await async_maybe_transform(body, image_create_params.ImageCreateParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/partnermax/resources/dealers/vehicles/vehicles.py b/src/partnermax/resources/dealers/vehicles/vehicles.py index bd70e94..91219e2 100644 --- a/src/partnermax/resources/dealers/vehicles/vehicles.py +++ b/src/partnermax/resources/dealers/vehicles/vehicles.py @@ -116,7 +116,7 @@ def create( certified_km: Certified odometer reading at intake, in kilometres. motornet_code: Motornet UNI code identifying the exact vehicle configuration. Must exist in the - used-vehicle catalogue at submission time; otherwise the call returns 422 + DealerMAX auto/VCOM catalogue at submission time; otherwise the call returns 422 `motornet_code_not_in_catalogue`. Partners may send a code from their own Motornet agreement or use the paid control-plane targa/VIN resolver before creating the vehicle. @@ -560,7 +560,7 @@ async def create( certified_km: Certified odometer reading at intake, in kilometres. motornet_code: Motornet UNI code identifying the exact vehicle configuration. Must exist in the - used-vehicle catalogue at submission time; otherwise the call returns 422 + DealerMAX auto/VCOM catalogue at submission time; otherwise the call returns 422 `motornet_code_not_in_catalogue`. Partners may send a code from their own Motornet agreement or use the paid control-plane targa/VIN resolver before creating the vehicle. diff --git a/src/partnermax/types/dealers/vehicle_bulk_params.py b/src/partnermax/types/dealers/vehicle_bulk_params.py index dc24e2d..943ec6a 100644 --- a/src/partnermax/types/dealers/vehicle_bulk_params.py +++ b/src/partnermax/types/dealers/vehicle_bulk_params.py @@ -40,10 +40,10 @@ class Vehicle(TypedDict, total=False): motornet_code: Required[str] """Motornet UNI code identifying the exact vehicle configuration. - Must exist in the used-vehicle catalogue at submission time; otherwise the call - returns 422 `motornet_code_not_in_catalogue`. Partners may send a code from - their own Motornet agreement or use the paid control-plane targa/VIN resolver - before creating the vehicle. + Must exist in the DealerMAX auto/VCOM catalogue at submission time; otherwise + the call returns 422 `motornet_code_not_in_catalogue`. Partners may send a code + from their own Motornet agreement or use the paid control-plane targa/VIN + resolver before creating the vehicle. """ plate: Required[str] diff --git a/src/partnermax/types/dealers/vehicle_create_params.py b/src/partnermax/types/dealers/vehicle_create_params.py index d1d4075..30d3887 100644 --- a/src/partnermax/types/dealers/vehicle_create_params.py +++ b/src/partnermax/types/dealers/vehicle_create_params.py @@ -17,10 +17,10 @@ class VehicleCreateParams(TypedDict, total=False): motornet_code: Required[str] """Motornet UNI code identifying the exact vehicle configuration. - Must exist in the used-vehicle catalogue at submission time; otherwise the call - returns 422 `motornet_code_not_in_catalogue`. Partners may send a code from - their own Motornet agreement or use the paid control-plane targa/VIN resolver - before creating the vehicle. + Must exist in the DealerMAX auto/VCOM catalogue at submission time; otherwise + the call returns 422 `motornet_code_not_in_catalogue`. Partners may send a code + from their own Motornet agreement or use the paid control-plane targa/VIN + resolver before creating the vehicle. """ plate: Required[str] diff --git a/src/partnermax/types/dealers/vehicles/image_create_params.py b/src/partnermax/types/dealers/vehicles/image_create_params.py index 567d5d2..1f284d3 100644 --- a/src/partnermax/types/dealers/vehicles/image_create_params.py +++ b/src/partnermax/types/dealers/vehicles/image_create_params.py @@ -4,13 +4,15 @@ from typing_extensions import Required, TypedDict +from ...._types import FileTypes + __all__ = ["ImageCreateParams"] class ImageCreateParams(TypedDict, total=False): dealer_id: Required[str] - file: Required[str] + file: Required[FileTypes] """The photo file. JPEG, PNG, or WebP, up to 15 MB. WebP is converted to PNG server-side. diff --git a/tests/api_resources/dealers/vehicles/test_images.py b/tests/api_resources/dealers/vehicles/test_images.py index 46f33dc..37084fb 100644 --- a/tests/api_resources/dealers/vehicles/test_images.py +++ b/tests/api_resources/dealers/vehicles/test_images.py @@ -23,7 +23,7 @@ def test_method_create(self, client: Partnermax) -> None: image = client.dealers.vehicles.images.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) assert_matches_type(VehicleImage, image, path=["response"]) @@ -33,7 +33,7 @@ def test_raw_response_create(self, client: Partnermax) -> None: response = client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) assert response.is_closed is True @@ -47,7 +47,7 @@ def test_streaming_response_create(self, client: Partnermax) -> None: with client.dealers.vehicles.images.with_streaming_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -64,14 +64,14 @@ def test_path_params_create(self, client: Partnermax) -> None: client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="", - file="file", + file=b"Example data", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): client.dealers.vehicles.images.with_raw_response.create( vehicle_id="", dealer_id="dealer_id", - file="file", + file=b"Example data", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -202,7 +202,7 @@ async def test_method_create(self, async_client: AsyncPartnermax) -> None: image = await async_client.dealers.vehicles.images.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) assert_matches_type(VehicleImage, image, path=["response"]) @@ -212,7 +212,7 @@ async def test_raw_response_create(self, async_client: AsyncPartnermax) -> None: response = await async_client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) assert response.is_closed is True @@ -226,7 +226,7 @@ async def test_streaming_response_create(self, async_client: AsyncPartnermax) -> async with async_client.dealers.vehicles.images.with_streaming_response.create( vehicle_id="vehicle_id", dealer_id="dealer_id", - file="file", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -243,14 +243,14 @@ async def test_path_params_create(self, async_client: AsyncPartnermax) -> None: await async_client.dealers.vehicles.images.with_raw_response.create( vehicle_id="vehicle_id", dealer_id="", - file="file", + file=b"Example data", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `vehicle_id` but received ''"): await async_client.dealers.vehicles.images.with_raw_response.create( vehicle_id="", dealer_id="dealer_id", - file="file", + file=b"Example data", ) @pytest.mark.skip(reason="Mock server tests are disabled")