From 63ab6f7d4fee33593fe9d3dfb6daa615bc00897e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:56:58 +0000 Subject: [PATCH 01/13] chore: update SDK settings --- .github/workflows/publish-pypi.yml | 31 +++++++++ .github/workflows/release-doctor.yml | 21 ++++++ .release-please-manifest.json | 3 + .stats.yml | 2 +- CONTRIBUTING.md | 4 +- README.md | 14 ++-- bin/check-release-environment | 21 ++++++ pyproject.toml | 6 +- release-please-config.json | 66 +++++++++++++++++++ src/unlayer/_version.py | 2 +- src/unlayer/resources/convert/convert.py | 8 +-- .../resources/convert/full_to_simple.py | 8 +-- .../resources/convert/simple_to_full.py | 8 +-- src/unlayer/resources/project.py | 8 +-- src/unlayer/resources/templates.py | 8 +-- src/unlayer/resources/workspaces.py | 8 +-- 16 files changed, 180 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/publish-pypi.yml create mode 100644 .github/workflows/release-doctor.yml create mode 100644 .release-please-manifest.json create mode 100644 bin/check-release-environment create mode 100644 release-please-config.json diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 0000000..44450e0 --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,31 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to PyPI in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/unlayer/unlayer-python/actions/workflows/publish-pypi.yml +name: Publish PyPI +on: + workflow_dispatch: + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Publish to PyPI + run: | + bash ./bin/publish-pypi + env: + PYPI_TOKEN: ${{ secrets.UNLAYER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..746dab3 --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,21 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'unlayer/unlayer-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v6 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + PYPI_TOKEN: ${{ secrets.UNLAYER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..1332969 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index c827f62..b0c7f3b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-e77bc881d5cb6c68be6f3d3861ed021b99f6cde45ee28d3511abe284d888262e.yml openapi_spec_hash: 7e7fae1b919c5d337e8b22be2a24d3ed -config_hash: ea554d62bfbb5e8ea43ebf0a274dec31 +config_hash: 00a7b0dff20113623ba749fe28286413 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8bc8eee..26c1b09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```sh -$ pip install git+ssh://git@github.com/stainless-sdks/unlayer-python.git +$ pip install git+ssh://git@github.com/unlayer/unlayer-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -120,7 +120,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/unlayer-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/unlayer/unlayer-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index 8e7620c..42a2e19 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ The full API of this library can be found in [api.md](api.md). ## Installation ```sh -# install from this staging repo -pip install git+ssh://git@github.com/stainless-sdks/unlayer-python.git +# install from the production repo +pip install git+ssh://git@github.com/unlayer/unlayer-python.git ``` > [!NOTE] @@ -83,8 +83,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from this staging repo -pip install 'unlayer[aiohttp] @ git+ssh://git@github.com/stainless-sdks/unlayer-python.git' +# install from the production repo +pip install 'unlayer[aiohttp] @ git+ssh://git@github.com/unlayer/unlayer-python.git' ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: @@ -349,9 +349,9 @@ project = response.parse() # get the object that `project.retrieve()` would hav print(project.data) ``` -These methods return an [`APIResponse`](https://github.com/stainless-sdks/unlayer-python/tree/main/src/unlayer/_response.py) object. +These methods return an [`APIResponse`](https://github.com/unlayer/unlayer-python/tree/main/src/unlayer/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/unlayer-python/tree/main/src/unlayer/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/unlayer/unlayer-python/tree/main/src/unlayer/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -457,7 +457,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/unlayer-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/unlayer/unlayer-python/issues) with questions, bugs, or suggestions. ### Determining the installed version diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..b845b0f --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +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 + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/pyproject.toml b/pyproject.toml index 2aa32b4..2bcff9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,8 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/stainless-sdks/unlayer-python" -Repository = "https://github.com/stainless-sdks/unlayer-python" +Homepage = "https://github.com/unlayer/unlayer-python" +Repository = "https://github.com/unlayer/unlayer-python" [project.optional-dependencies] aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] @@ -126,7 +126,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/stainless-sdks/unlayer-python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/unlayer/unlayer-python/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..d1ec05c --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,66 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "python", + "extra-files": [ + "src/unlayer/_version.py" + ] +} \ No newline at end of file diff --git a/src/unlayer/_version.py b/src/unlayer/_version.py index c9e1c88..b9adb5b 100644 --- a/src/unlayer/_version.py +++ b/src/unlayer/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "unlayer" -__version__ = "0.0.1" +__version__ = "0.0.1" # x-release-please-version diff --git a/src/unlayer/resources/convert/convert.py b/src/unlayer/resources/convert/convert.py index c303200..08eee7c 100644 --- a/src/unlayer/resources/convert/convert.py +++ b/src/unlayer/resources/convert/convert.py @@ -39,7 +39,7 @@ def with_raw_response(self) -> ConvertResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return ConvertResourceWithRawResponse(self) @@ -48,7 +48,7 @@ def with_streaming_response(self) -> ConvertResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return ConvertResourceWithStreamingResponse(self) @@ -68,7 +68,7 @@ def with_raw_response(self) -> AsyncConvertResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return AsyncConvertResourceWithRawResponse(self) @@ -77,7 +77,7 @@ def with_streaming_response(self) -> AsyncConvertResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return AsyncConvertResourceWithStreamingResponse(self) diff --git a/src/unlayer/resources/convert/full_to_simple.py b/src/unlayer/resources/convert/full_to_simple.py index 0d793c7..d5901f4 100644 --- a/src/unlayer/resources/convert/full_to_simple.py +++ b/src/unlayer/resources/convert/full_to_simple.py @@ -30,7 +30,7 @@ def with_raw_response(self) -> FullToSimpleResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return FullToSimpleResourceWithRawResponse(self) @@ -39,7 +39,7 @@ def with_streaming_response(self) -> FullToSimpleResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return FullToSimpleResourceWithStreamingResponse(self) @@ -97,7 +97,7 @@ def with_raw_response(self) -> AsyncFullToSimpleResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return AsyncFullToSimpleResourceWithRawResponse(self) @@ -106,7 +106,7 @@ def with_streaming_response(self) -> AsyncFullToSimpleResourceWithStreamingRespo """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return AsyncFullToSimpleResourceWithStreamingResponse(self) diff --git a/src/unlayer/resources/convert/simple_to_full.py b/src/unlayer/resources/convert/simple_to_full.py index 29db149..d9b634f 100644 --- a/src/unlayer/resources/convert/simple_to_full.py +++ b/src/unlayer/resources/convert/simple_to_full.py @@ -30,7 +30,7 @@ def with_raw_response(self) -> SimpleToFullResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return SimpleToFullResourceWithRawResponse(self) @@ -39,7 +39,7 @@ def with_streaming_response(self) -> SimpleToFullResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return SimpleToFullResourceWithStreamingResponse(self) @@ -92,7 +92,7 @@ def with_raw_response(self) -> AsyncSimpleToFullResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return AsyncSimpleToFullResourceWithRawResponse(self) @@ -101,7 +101,7 @@ def with_streaming_response(self) -> AsyncSimpleToFullResourceWithStreamingRespo """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return AsyncSimpleToFullResourceWithStreamingResponse(self) diff --git a/src/unlayer/resources/project.py b/src/unlayer/resources/project.py index 6ff7918..6de8cf8 100644 --- a/src/unlayer/resources/project.py +++ b/src/unlayer/resources/project.py @@ -28,7 +28,7 @@ def with_raw_response(self) -> ProjectResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return ProjectResourceWithRawResponse(self) @@ -37,7 +37,7 @@ def with_streaming_response(self) -> ProjectResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return ProjectResourceWithStreamingResponse(self) @@ -86,7 +86,7 @@ def with_raw_response(self) -> AsyncProjectResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return AsyncProjectResourceWithRawResponse(self) @@ -95,7 +95,7 @@ def with_streaming_response(self) -> AsyncProjectResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return AsyncProjectResourceWithStreamingResponse(self) diff --git a/src/unlayer/resources/templates.py b/src/unlayer/resources/templates.py index 4c0708a..68389c9 100644 --- a/src/unlayer/resources/templates.py +++ b/src/unlayer/resources/templates.py @@ -32,7 +32,7 @@ def with_raw_response(self) -> TemplatesResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return TemplatesResourceWithRawResponse(self) @@ -41,7 +41,7 @@ def with_streaming_response(self) -> TemplatesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return TemplatesResourceWithStreamingResponse(self) @@ -154,7 +154,7 @@ def with_raw_response(self) -> AsyncTemplatesResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return AsyncTemplatesResourceWithRawResponse(self) @@ -163,7 +163,7 @@ def with_streaming_response(self) -> AsyncTemplatesResourceWithStreamingResponse """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return AsyncTemplatesResourceWithStreamingResponse(self) diff --git a/src/unlayer/resources/workspaces.py b/src/unlayer/resources/workspaces.py index 494ccea..4f4f5cd 100644 --- a/src/unlayer/resources/workspaces.py +++ b/src/unlayer/resources/workspaces.py @@ -27,7 +27,7 @@ def with_raw_response(self) -> WorkspacesResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return WorkspacesResourceWithRawResponse(self) @@ -36,7 +36,7 @@ def with_streaming_response(self) -> WorkspacesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return WorkspacesResourceWithStreamingResponse(self) @@ -100,7 +100,7 @@ def with_raw_response(self) -> AsyncWorkspacesResourceWithRawResponse: 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/stainless-sdks/unlayer-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ return AsyncWorkspacesResourceWithRawResponse(self) @@ -109,7 +109,7 @@ def with_streaming_response(self) -> AsyncWorkspacesResourceWithStreamingRespons """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/unlayer-python#with_streaming_response + For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ return AsyncWorkspacesResourceWithStreamingResponse(self) From 93ff5c1e130c8739bdc383b5af9b6bab6453483e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:57:16 +0000 Subject: [PATCH 02/13] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b0c7f3b..3a44165 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-e77bc881d5cb6c68be6f3d3861ed021b99f6cde45ee28d3511abe284d888262e.yml openapi_spec_hash: 7e7fae1b919c5d337e8b22be2a24d3ed -config_hash: 00a7b0dff20113623ba749fe28286413 +config_hash: 344aa63165862b95d788ad377dd902e3 From 52e5d054a25723d3798cd8a571cd48bc7625bce9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:57:36 +0000 Subject: [PATCH 03/13] chore: update SDK settings --- .stats.yml | 2 +- README.md | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3a44165..c325690 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-e77bc881d5cb6c68be6f3d3861ed021b99f6cde45ee28d3511abe284d888262e.yml openapi_spec_hash: 7e7fae1b919c5d337e8b22be2a24d3ed -config_hash: 344aa63165862b95d788ad377dd902e3 +config_hash: 68f088239d13d9983e4913c6b8396c0d diff --git a/README.md b/README.md index 42a2e19..ddb73df 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,10 @@ The full API of this library can be found in [api.md](api.md). ## Installation ```sh -# install from the production repo -pip install git+ssh://git@github.com/unlayer/unlayer-python.git +# install from PyPI +pip install unlayer ``` -> [!NOTE] -> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install unlayer` - ## Usage The full API of this library can be found in [api.md](api.md). @@ -83,8 +80,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from the production repo -pip install 'unlayer[aiohttp] @ git+ssh://git@github.com/unlayer/unlayer-python.git' +# install from PyPI +pip install unlayer[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: From 0bb4c03ff768646ccd85bbc7c3def0391156dc17 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:57:53 +0000 Subject: [PATCH 04/13] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index c325690..5702403 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-e77bc881d5cb6c68be6f3d3861ed021b99f6cde45ee28d3511abe284d888262e.yml openapi_spec_hash: 7e7fae1b919c5d337e8b22be2a24d3ed -config_hash: 68f088239d13d9983e4913c6b8396c0d +config_hash: 87d560e6d481bc04dc9310719a5eb946 From e895c53f0565b55ca484e65fdcacbe9db6627d4d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:57:30 +0000 Subject: [PATCH 05/13] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5702403..5a7e305 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-e77bc881d5cb6c68be6f3d3861ed021b99f6cde45ee28d3511abe284d888262e.yml -openapi_spec_hash: 7e7fae1b919c5d337e8b22be2a24d3ed +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-05f06124acf955282470ca7d863e8d10c5dd36cbde746d154482e9972277cd03.yml +openapi_spec_hash: 21532f2d9416a6e55bf9107df0948cd8 config_hash: 87d560e6d481bc04dc9310719a5eb946 From e835dc37eab896b138333888f423ba0aae6f0958 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 04:06:17 +0000 Subject: [PATCH 06/13] chore: update mock server docs --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26c1b09..8f8a253 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,8 +88,7 @@ $ pip install ./path-to-wheel-file.whl Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. ```sh -# you will need npm installed -$ npx prism mock path/to/your/openapi.yml +$ ./scripts/mock ``` ```sh From 1255c9b3e6bd708eefb21313c022a1018a78fc3e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:56:45 +0000 Subject: [PATCH 07/13] feat(api): api update --- .stats.yml | 6 +- README.md | 26 +- src/unlayer/__init__.py | 14 +- src/unlayer/_client.py | 234 ++++++++-------- src/unlayer/resources/project.py | 10 +- src/unlayer/resources/templates.py | 24 +- src/unlayer/resources/workspaces.py | 24 +- src/unlayer/types/project_retrieve_params.py | 6 +- src/unlayer/types/template_list_params.py | 8 +- src/unlayer/types/template_retrieve_params.py | 6 +- tests/api_resources/test_project.py | 26 +- tests/api_resources/test_templates.py | 48 ++-- tests/conftest.py | 6 +- tests/test_client.py | 256 +++++++----------- 14 files changed, 315 insertions(+), 379 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5a7e305..2fb1d6b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-05f06124acf955282470ca7d863e8d10c5dd36cbde746d154482e9972277cd03.yml -openapi_spec_hash: 21532f2d9416a6e55bf9107df0948cd8 -config_hash: 87d560e6d481bc04dc9310719a5eb946 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-14707e371ca64ee082f425e10f8bd4b7d9e2eeb28d6e69daad66902abb1b8b6b.yml +openapi_spec_hash: 8198d0442f4736109bf6c49a5a8697ab +config_hash: 144077318a24cfbe9ce6da795a628a80 diff --git a/README.md b/README.md index ddb73df..2974eba 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,7 @@ import os from unlayer import Unlayer client = Unlayer( - access_token=os.environ.get("UNLAYER_ACCESS_TOKEN"), # This is the default and can be omitted - # or 'production' | 'qa' | 'dev'; defaults to "production". - environment="stage", + api_key=os.environ.get("UNLAYER_API_KEY"), # This is the default and can be omitted ) project = client.project.retrieve( @@ -40,10 +38,10 @@ project = client.project.retrieve( print(project.data) ``` -While you can provide a `access_token` keyword argument, +While you can provide an `api_key` keyword argument, we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) -to add `UNLAYER_ACCESS_TOKEN="My Access Token"` to your `.env` file -so that your Access Token is not stored in source control. +to add `UNLAYER_API_KEY="My API Key"` to your `.env` file +so that your API Key is not stored in source control. ## Async usage @@ -55,9 +53,7 @@ import asyncio from unlayer import AsyncUnlayer client = AsyncUnlayer( - access_token=os.environ.get("UNLAYER_ACCESS_TOKEN"), # This is the default and can be omitted - # or 'production' | 'qa' | 'dev'; defaults to "production". - environment="stage", + api_key=os.environ.get("UNLAYER_API_KEY"), # This is the default and can be omitted ) @@ -95,9 +91,7 @@ from unlayer import AsyncUnlayer async def main() -> None: async with AsyncUnlayer( - access_token=os.environ.get( - "UNLAYER_ACCESS_TOKEN" - ), # This is the default and can be omitted + api_key=os.environ.get("UNLAYER_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: project = await client.project.retrieve( @@ -132,8 +126,8 @@ client = Unlayer() all_templates = [] # Automatically fetches more pages as needed. for template in client.templates.list( - project_id="your-project-id", limit=10, + project_id="your-project-id", ): # Do something with template here all_templates.append(template) @@ -153,8 +147,8 @@ async def main() -> None: all_templates = [] # Iterate through items across all pages, issuing requests as needed. async for template in client.templates.list( - project_id="your-project-id", limit=10, + project_id="your-project-id", ): all_templates.append(template) print(all_templates) @@ -167,8 +161,8 @@ Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get ```python first_page = await client.templates.list( - project_id="your-project-id", limit=10, + project_id="your-project-id", ) if first_page.has_next_page(): print(f"will fetch next page using these details: {first_page.next_page_info()}") @@ -182,8 +176,8 @@ Or just work directly with the returned data: ```python first_page = await client.templates.list( - project_id="your-project-id", limit=10, + project_id="your-project-id", ) print(f"next page cursor: {first_page.next_cursor}") # => "next page cursor: ..." diff --git a/src/unlayer/__init__.py b/src/unlayer/__init__.py index 196b04d..37d9b74 100644 --- a/src/unlayer/__init__.py +++ b/src/unlayer/__init__.py @@ -5,18 +5,7 @@ from . import types from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given from ._utils import file_from_path -from ._client import ( - ENVIRONMENTS, - Client, - Stream, - Timeout, - Unlayer, - Transport, - AsyncClient, - AsyncStream, - AsyncUnlayer, - RequestOptions, -) +from ._client import Client, Stream, Timeout, Unlayer, Transport, AsyncClient, AsyncStream, AsyncUnlayer, RequestOptions from ._models import BaseModel from ._version import __title__, __version__ from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse @@ -74,7 +63,6 @@ "AsyncStream", "Unlayer", "AsyncUnlayer", - "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/unlayer/_client.py b/src/unlayer/_client.py index 01b60d9..4c34931 100644 --- a/src/unlayer/_client.py +++ b/src/unlayer/_client.py @@ -3,8 +3,8 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING, Any, Dict, Mapping, cast -from typing_extensions import Self, Literal, override +from typing import TYPE_CHECKING, Any, Mapping +from typing_extensions import Self, override import httpx @@ -12,6 +12,7 @@ from ._qs import Querystring from ._types import ( Omit, + Headers, Timeout, NotGiven, Transport, @@ -23,7 +24,7 @@ from ._compat import cached_property from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream -from ._exceptions import UnlayerError, APIStatusError +from ._exceptions import APIStatusError from ._base_client import ( DEFAULT_MAX_RETRIES, SyncAPIClient, @@ -37,38 +38,22 @@ from .resources.workspaces import WorkspacesResource, AsyncWorkspacesResource from .resources.convert.convert import ConvertResource, AsyncConvertResource -__all__ = [ - "ENVIRONMENTS", - "Timeout", - "Transport", - "ProxiesTypes", - "RequestOptions", - "Unlayer", - "AsyncUnlayer", - "Client", - "AsyncClient", -] - -ENVIRONMENTS: Dict[str, str] = { - "production": "https://api.unlayer.com", - "stage": "https://api.stage.unlayer.com", - "qa": "https://api.qa.unlayer.com", - "dev": "https://api.dev.unlayer.com", -} +__all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Unlayer", "AsyncUnlayer", "Client", "AsyncClient"] class Unlayer(SyncAPIClient): # client options - access_token: str - - _environment: Literal["production", "stage", "qa", "dev"] | NotGiven + api_key: str | None + personal_access_token: str | None + project_id: str | None def __init__( self, *, - access_token: str | None = None, - environment: Literal["production", "stage", "qa", "dev"] | NotGiven = not_given, - base_url: str | httpx.URL | None | NotGiven = not_given, + api_key: str | None = None, + personal_access_token: str | None = None, + project_id: str | None = None, + base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -89,41 +74,27 @@ def __init__( ) -> None: """Construct a new synchronous Unlayer client instance. - This automatically infers the `access_token` argument from the `UNLAYER_ACCESS_TOKEN` environment variable if it is not provided. + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `api_key` from `UNLAYER_API_KEY` + - `personal_access_token` from `UNLAYER_PERSONAL_ACCESS_TOKEN` + - `project_id` from `UNLAYER_PROJECT_ID` """ - if access_token is None: - access_token = os.environ.get("UNLAYER_ACCESS_TOKEN") - if access_token is None: - raise UnlayerError( - "The access_token client option must be set either by passing access_token to the client or by setting the UNLAYER_ACCESS_TOKEN environment variable" - ) - self.access_token = access_token - - self._environment = environment - - base_url_env = os.environ.get("UNLAYER_BASE_URL") - if is_given(base_url) and base_url is not None: - # cast required because mypy doesn't understand the type narrowing - base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] - elif is_given(environment): - if base_url_env and base_url is not None: - raise ValueError( - "Ambiguous URL; The `UNLAYER_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", - ) - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc - elif base_url_env is not None: - base_url = base_url_env - else: - self._environment = environment = "production" - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc + if api_key is None: + api_key = os.environ.get("UNLAYER_API_KEY") + self.api_key = api_key + + if personal_access_token is None: + personal_access_token = os.environ.get("UNLAYER_PERSONAL_ACCESS_TOKEN") + self.personal_access_token = personal_access_token + + if project_id is None: + project_id = os.environ.get("UNLAYER_PROJECT_ID") + self.project_id = project_id + + if base_url is None: + base_url = os.environ.get("UNLAYER_BASE_URL") + if base_url is None: + base_url = f"https://api.unlayer.com" super().__init__( version=__version__, @@ -176,8 +147,21 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - access_token = self.access_token - return {"Authorization": f"Bearer {access_token}"} + return {**self._api_key_auth, **self._personal_access_token_auth} + + @property + def _api_key_auth(self) -> dict[str, str]: + api_key = self.api_key + if api_key is None: + return {} + return {"Authorization": f"Bearer {api_key}"} + + @property + def _personal_access_token_auth(self) -> dict[str, str]: + personal_access_token = self.personal_access_token + if personal_access_token is None: + return {} + return {"Authorization": f"Bearer {personal_access_token}"} @property @override @@ -185,14 +169,25 @@ def default_headers(self) -> dict[str, str | Omit]: return { **super().default_headers, "X-Stainless-Async": "false", + "X-Project-ID": self.project_id if self.project_id is not None else Omit(), **self._custom_headers, } + @override + def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: + if headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit): + return + + raise TypeError( + '"Could not resolve authentication method. Expected either api_key or personal_access_token to be set. Or for one of the `Authorization` or `Authorization` headers to be explicitly omitted"' + ) + def copy( self, *, - access_token: str | None = None, - environment: Literal["production", "stage", "qa", "dev"] | None = None, + api_key: str | None = None, + personal_access_token: str | None = None, + project_id: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, @@ -226,9 +221,10 @@ def copy( http_client = http_client or self._client return self.__class__( - access_token=access_token or self.access_token, + api_key=api_key or self.api_key, + personal_access_token=personal_access_token or self.personal_access_token, + project_id=project_id or self.project_id, base_url=base_url or self.base_url, - environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, @@ -277,16 +273,17 @@ def _make_status_error( class AsyncUnlayer(AsyncAPIClient): # client options - access_token: str - - _environment: Literal["production", "stage", "qa", "dev"] | NotGiven + api_key: str | None + personal_access_token: str | None + project_id: str | None def __init__( self, *, - access_token: str | None = None, - environment: Literal["production", "stage", "qa", "dev"] | NotGiven = not_given, - base_url: str | httpx.URL | None | NotGiven = not_given, + api_key: str | None = None, + personal_access_token: str | None = None, + project_id: str | None = None, + base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -307,41 +304,27 @@ def __init__( ) -> None: """Construct a new async AsyncUnlayer client instance. - This automatically infers the `access_token` argument from the `UNLAYER_ACCESS_TOKEN` environment variable if it is not provided. + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `api_key` from `UNLAYER_API_KEY` + - `personal_access_token` from `UNLAYER_PERSONAL_ACCESS_TOKEN` + - `project_id` from `UNLAYER_PROJECT_ID` """ - if access_token is None: - access_token = os.environ.get("UNLAYER_ACCESS_TOKEN") - if access_token is None: - raise UnlayerError( - "The access_token client option must be set either by passing access_token to the client or by setting the UNLAYER_ACCESS_TOKEN environment variable" - ) - self.access_token = access_token - - self._environment = environment - - base_url_env = os.environ.get("UNLAYER_BASE_URL") - if is_given(base_url) and base_url is not None: - # cast required because mypy doesn't understand the type narrowing - base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] - elif is_given(environment): - if base_url_env and base_url is not None: - raise ValueError( - "Ambiguous URL; The `UNLAYER_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", - ) - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc - elif base_url_env is not None: - base_url = base_url_env - else: - self._environment = environment = "production" - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc + if api_key is None: + api_key = os.environ.get("UNLAYER_API_KEY") + self.api_key = api_key + + if personal_access_token is None: + personal_access_token = os.environ.get("UNLAYER_PERSONAL_ACCESS_TOKEN") + self.personal_access_token = personal_access_token + + if project_id is None: + project_id = os.environ.get("UNLAYER_PROJECT_ID") + self.project_id = project_id + + if base_url is None: + base_url = os.environ.get("UNLAYER_BASE_URL") + if base_url is None: + base_url = f"https://api.unlayer.com" super().__init__( version=__version__, @@ -394,8 +377,21 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - access_token = self.access_token - return {"Authorization": f"Bearer {access_token}"} + return {**self._api_key_auth, **self._personal_access_token_auth} + + @property + def _api_key_auth(self) -> dict[str, str]: + api_key = self.api_key + if api_key is None: + return {} + return {"Authorization": f"Bearer {api_key}"} + + @property + def _personal_access_token_auth(self) -> dict[str, str]: + personal_access_token = self.personal_access_token + if personal_access_token is None: + return {} + return {"Authorization": f"Bearer {personal_access_token}"} @property @override @@ -403,14 +399,25 @@ def default_headers(self) -> dict[str, str | Omit]: return { **super().default_headers, "X-Stainless-Async": f"async:{get_async_library()}", + "X-Project-ID": self.project_id if self.project_id is not None else Omit(), **self._custom_headers, } + @override + def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: + if headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit): + return + + raise TypeError( + '"Could not resolve authentication method. Expected either api_key or personal_access_token to be set. Or for one of the `Authorization` or `Authorization` headers to be explicitly omitted"' + ) + def copy( self, *, - access_token: str | None = None, - environment: Literal["production", "stage", "qa", "dev"] | None = None, + api_key: str | None = None, + personal_access_token: str | None = None, + project_id: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, @@ -444,9 +451,10 @@ def copy( http_client = http_client or self._client return self.__class__( - access_token=access_token or self.access_token, + api_key=api_key or self.api_key, + personal_access_token=personal_access_token or self.personal_access_token, + project_id=project_id or self.project_id, base_url=base_url or self.base_url, - environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, diff --git a/src/unlayer/resources/project.py b/src/unlayer/resources/project.py index 6de8cf8..af286c2 100644 --- a/src/unlayer/resources/project.py +++ b/src/unlayer/resources/project.py @@ -5,7 +5,7 @@ import httpx from ..types import project_retrieve_params -from .._types import Body, Query, Headers, NotGiven, not_given +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -44,7 +44,7 @@ def with_streaming_response(self) -> ProjectResourceWithStreamingResponse: def retrieve( self, *, - project_id: str, + project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -56,7 +56,7 @@ def retrieve( Get project details for the specified project. Args: - project_id: The project ID + project_id: The project ID (required for PAT auth, auto-resolved for API key auth) extra_headers: Send extra headers @@ -102,7 +102,7 @@ def with_streaming_response(self) -> AsyncProjectResourceWithStreamingResponse: async def retrieve( self, *, - project_id: str, + project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -114,7 +114,7 @@ async def retrieve( Get project details for the specified project. Args: - project_id: The project ID + project_id: The project ID (required for PAT auth, auto-resolved for API key auth) extra_headers: Send extra headers diff --git a/src/unlayer/resources/templates.py b/src/unlayer/resources/templates.py index 68389c9..e41c931 100644 --- a/src/unlayer/resources/templates.py +++ b/src/unlayer/resources/templates.py @@ -49,7 +49,7 @@ def retrieve( self, id: str, *, - project_id: str, + project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -61,7 +61,7 @@ def retrieve( Get template by ID. Args: - project_id: The project ID + project_id: The project ID (required for PAT auth, auto-resolved for API key auth) extra_headers: Send extra headers @@ -88,11 +88,11 @@ def retrieve( def list( self, *, - project_id: str, cursor: str | Omit = omit, display_mode: Literal["email", "web", "document"] | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -106,8 +106,6 @@ def list( order by update time. Args: - project_id: The project ID to list templates for - cursor: Pagination cursor from previous response display_mode: Filter by template type @@ -116,6 +114,8 @@ def list( name: Filter by name (case-insensitive search) + project_id: The project ID to list templates for + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -134,11 +134,11 @@ def list( timeout=timeout, query=maybe_transform( { - "project_id": project_id, "cursor": cursor, "display_mode": display_mode, "limit": limit, "name": name, + "project_id": project_id, }, template_list_params.TemplateListParams, ), @@ -171,7 +171,7 @@ async def retrieve( self, id: str, *, - project_id: str, + project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -183,7 +183,7 @@ async def retrieve( Get template by ID. Args: - project_id: The project ID + project_id: The project ID (required for PAT auth, auto-resolved for API key auth) extra_headers: Send extra headers @@ -212,11 +212,11 @@ async def retrieve( def list( self, *, - project_id: str, cursor: str | Omit = omit, display_mode: Literal["email", "web", "document"] | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, + project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -230,8 +230,6 @@ def list( order by update time. Args: - project_id: The project ID to list templates for - cursor: Pagination cursor from previous response display_mode: Filter by template type @@ -240,6 +238,8 @@ def list( name: Filter by name (case-insensitive search) + project_id: The project ID to list templates for + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -258,11 +258,11 @@ def list( timeout=timeout, query=maybe_transform( { - "project_id": project_id, "cursor": cursor, "display_mode": display_mode, "limit": limit, "name": name, + "project_id": project_id, }, template_list_params.TemplateListParams, ), diff --git a/src/unlayer/resources/workspaces.py b/src/unlayer/resources/workspaces.py index 4f4f5cd..3bce476 100644 --- a/src/unlayer/resources/workspaces.py +++ b/src/unlayer/resources/workspaces.py @@ -51,8 +51,10 @@ def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> WorkspaceRetrieveResponse: - """ - Get a specific workspace by ID with its projects. + """Get a specific workspace by ID with its projects. + + Requires a Personal Access + Token (PAT). Args: extra_headers: Send extra headers @@ -83,7 +85,11 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> WorkspaceListResponse: - """Get all workspaces accessible by the current token.""" + """Get all workspaces accessible by the current token. + + Requires a Personal Access + Token (PAT). + """ return self._get( "/v3/workspaces", options=make_request_options( @@ -124,8 +130,10 @@ async def retrieve( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> WorkspaceRetrieveResponse: - """ - Get a specific workspace by ID with its projects. + """Get a specific workspace by ID with its projects. + + Requires a Personal Access + Token (PAT). Args: extra_headers: Send extra headers @@ -156,7 +164,11 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> WorkspaceListResponse: - """Get all workspaces accessible by the current token.""" + """Get all workspaces accessible by the current token. + + Requires a Personal Access + Token (PAT). + """ return await self._get( "/v3/workspaces", options=make_request_options( diff --git a/src/unlayer/types/project_retrieve_params.py b/src/unlayer/types/project_retrieve_params.py index d2299de..f18748a 100644 --- a/src/unlayer/types/project_retrieve_params.py +++ b/src/unlayer/types/project_retrieve_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Required, Annotated, TypedDict +from typing_extensions import Annotated, TypedDict from .._utils import PropertyInfo @@ -10,5 +10,5 @@ class ProjectRetrieveParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] - """The project ID""" + project_id: Annotated[str, PropertyInfo(alias="projectId")] + """The project ID (required for PAT auth, auto-resolved for API key auth)""" diff --git a/src/unlayer/types/template_list_params.py b/src/unlayer/types/template_list_params.py index afb5c25..79a4a48 100644 --- a/src/unlayer/types/template_list_params.py +++ b/src/unlayer/types/template_list_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Literal, Required, Annotated, TypedDict +from typing_extensions import Literal, Annotated, TypedDict from .._utils import PropertyInfo @@ -10,9 +10,6 @@ class TemplateListParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] - """The project ID to list templates for""" - cursor: str """Pagination cursor from previous response""" @@ -24,3 +21,6 @@ class TemplateListParams(TypedDict, total=False): name: str """Filter by name (case-insensitive search)""" + + project_id: Annotated[str, PropertyInfo(alias="projectId")] + """The project ID to list templates for""" diff --git a/src/unlayer/types/template_retrieve_params.py b/src/unlayer/types/template_retrieve_params.py index c7ca6cf..d5af5fa 100644 --- a/src/unlayer/types/template_retrieve_params.py +++ b/src/unlayer/types/template_retrieve_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing_extensions import Required, Annotated, TypedDict +from typing_extensions import Annotated, TypedDict from .._utils import PropertyInfo @@ -10,5 +10,5 @@ class TemplateRetrieveParams(TypedDict, total=False): - project_id: Required[Annotated[str, PropertyInfo(alias="projectId")]] - """The project ID""" + project_id: Annotated[str, PropertyInfo(alias="projectId")] + """The project ID (required for PAT auth, auto-resolved for API key auth)""" diff --git a/tests/api_resources/test_project.py b/tests/api_resources/test_project.py index 5719de6..58151bf 100644 --- a/tests/api_resources/test_project.py +++ b/tests/api_resources/test_project.py @@ -19,6 +19,11 @@ class TestProject: @parametrize def test_method_retrieve(self, client: Unlayer) -> None: + project = client.project.retrieve() + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + + @parametrize + def test_method_retrieve_with_all_params(self, client: Unlayer) -> None: project = client.project.retrieve( project_id="projectId", ) @@ -26,9 +31,7 @@ def test_method_retrieve(self, client: Unlayer) -> None: @parametrize def test_raw_response_retrieve(self, client: Unlayer) -> None: - response = client.project.with_raw_response.retrieve( - project_id="projectId", - ) + response = client.project.with_raw_response.retrieve() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -37,9 +40,7 @@ def test_raw_response_retrieve(self, client: Unlayer) -> None: @parametrize def test_streaming_response_retrieve(self, client: Unlayer) -> None: - with client.project.with_streaming_response.retrieve( - project_id="projectId", - ) as response: + with client.project.with_streaming_response.retrieve() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -56,6 +57,11 @@ class TestAsyncProject: @parametrize async def test_method_retrieve(self, async_client: AsyncUnlayer) -> None: + project = await async_client.project.retrieve() + assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) + + @parametrize + async def test_method_retrieve_with_all_params(self, async_client: AsyncUnlayer) -> None: project = await async_client.project.retrieve( project_id="projectId", ) @@ -63,9 +69,7 @@ async def test_method_retrieve(self, async_client: AsyncUnlayer) -> None: @parametrize async def test_raw_response_retrieve(self, async_client: AsyncUnlayer) -> None: - response = await async_client.project.with_raw_response.retrieve( - project_id="projectId", - ) + response = await async_client.project.with_raw_response.retrieve() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -74,9 +78,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncUnlayer) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncUnlayer) -> None: - async with async_client.project.with_streaming_response.retrieve( - project_id="projectId", - ) as response: + async with async_client.project.with_streaming_response.retrieve() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_templates.py b/tests/api_resources/test_templates.py index 4a2291b..bea7f32 100644 --- a/tests/api_resources/test_templates.py +++ b/tests/api_resources/test_templates.py @@ -20,6 +20,13 @@ class TestTemplates: @parametrize def test_method_retrieve(self, client: Unlayer) -> None: + template = client.templates.retrieve( + id="id", + ) + assert_matches_type(TemplateRetrieveResponse, template, path=["response"]) + + @parametrize + def test_method_retrieve_with_all_params(self, client: Unlayer) -> None: template = client.templates.retrieve( id="id", project_id="projectId", @@ -30,7 +37,6 @@ def test_method_retrieve(self, client: Unlayer) -> None: def test_raw_response_retrieve(self, client: Unlayer) -> None: response = client.templates.with_raw_response.retrieve( id="id", - project_id="projectId", ) assert response.is_closed is True @@ -42,7 +48,6 @@ def test_raw_response_retrieve(self, client: Unlayer) -> None: def test_streaming_response_retrieve(self, client: Unlayer) -> None: with client.templates.with_streaming_response.retrieve( id="id", - project_id="projectId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -57,32 +62,27 @@ def test_path_params_retrieve(self, client: Unlayer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): client.templates.with_raw_response.retrieve( id="", - project_id="projectId", ) @parametrize def test_method_list(self, client: Unlayer) -> None: - template = client.templates.list( - project_id="projectId", - ) + template = client.templates.list() assert_matches_type(SyncCursorPage[TemplateListResponse], template, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Unlayer) -> None: template = client.templates.list( - project_id="projectId", cursor="cursor", display_mode="email", limit=1, name="name", + project_id="projectId", ) assert_matches_type(SyncCursorPage[TemplateListResponse], template, path=["response"]) @parametrize def test_raw_response_list(self, client: Unlayer) -> None: - response = client.templates.with_raw_response.list( - project_id="projectId", - ) + response = client.templates.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -91,9 +91,7 @@ def test_raw_response_list(self, client: Unlayer) -> None: @parametrize def test_streaming_response_list(self, client: Unlayer) -> None: - with client.templates.with_streaming_response.list( - project_id="projectId", - ) as response: + with client.templates.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -110,6 +108,13 @@ class TestAsyncTemplates: @parametrize async def test_method_retrieve(self, async_client: AsyncUnlayer) -> None: + template = await async_client.templates.retrieve( + id="id", + ) + assert_matches_type(TemplateRetrieveResponse, template, path=["response"]) + + @parametrize + async def test_method_retrieve_with_all_params(self, async_client: AsyncUnlayer) -> None: template = await async_client.templates.retrieve( id="id", project_id="projectId", @@ -120,7 +125,6 @@ async def test_method_retrieve(self, async_client: AsyncUnlayer) -> None: async def test_raw_response_retrieve(self, async_client: AsyncUnlayer) -> None: response = await async_client.templates.with_raw_response.retrieve( id="id", - project_id="projectId", ) assert response.is_closed is True @@ -132,7 +136,6 @@ async def test_raw_response_retrieve(self, async_client: AsyncUnlayer) -> None: async def test_streaming_response_retrieve(self, async_client: AsyncUnlayer) -> None: async with async_client.templates.with_streaming_response.retrieve( id="id", - project_id="projectId", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -147,32 +150,27 @@ async def test_path_params_retrieve(self, async_client: AsyncUnlayer) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): await async_client.templates.with_raw_response.retrieve( id="", - project_id="projectId", ) @parametrize async def test_method_list(self, async_client: AsyncUnlayer) -> None: - template = await async_client.templates.list( - project_id="projectId", - ) + template = await async_client.templates.list() assert_matches_type(AsyncCursorPage[TemplateListResponse], template, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncUnlayer) -> None: template = await async_client.templates.list( - project_id="projectId", cursor="cursor", display_mode="email", limit=1, name="name", + project_id="projectId", ) assert_matches_type(AsyncCursorPage[TemplateListResponse], template, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncUnlayer) -> None: - response = await async_client.templates.with_raw_response.list( - project_id="projectId", - ) + response = await async_client.templates.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -181,9 +179,7 @@ async def test_raw_response_list(self, async_client: AsyncUnlayer) -> None: @parametrize async def test_streaming_response_list(self, async_client: AsyncUnlayer) -> None: - async with async_client.templates.with_streaming_response.list( - project_id="projectId", - ) as response: + async with async_client.templates.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/conftest.py b/tests/conftest.py index 0fed985..fc6ba41 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,7 +45,7 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -access_token = "My Access Token" +api_key = "My API Key" @pytest.fixture(scope="session") @@ -54,7 +54,7 @@ def client(request: FixtureRequest) -> Iterator[Unlayer]: if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - with Unlayer(base_url=base_url, access_token=access_token, _strict_response_validation=strict) as client: + with Unlayer(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: yield client @@ -79,6 +79,6 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncUnlayer]: raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") async with AsyncUnlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=strict, http_client=http_client + base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client ) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index 97ed357..93072a2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,7 +23,7 @@ from unlayer._types import Omit from unlayer._utils import asyncify from unlayer._models import BaseModel, FinalRequestOptions -from unlayer._exceptions import UnlayerError, APIStatusError, APITimeoutError, APIResponseValidationError +from unlayer._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError from unlayer._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, @@ -39,7 +39,7 @@ T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -access_token = "My Access Token" +api_key = "My API Key" def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: @@ -136,9 +136,9 @@ def test_copy(self, client: Unlayer) -> None: copied = client.copy() assert id(copied) != id(client) - copied = client.copy(access_token="another My Access Token") - assert copied.access_token == "another My Access Token" - assert client.access_token == "My Access Token" + copied = client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert client.api_key == "My API Key" def test_copy_default_options(self, client: Unlayer) -> None: # options that have a default are overridden correctly @@ -158,10 +158,7 @@ def test_copy_default_options(self, client: Unlayer) -> None: def test_copy_default_headers(self) -> None: client = Unlayer( - base_url=base_url, - access_token=access_token, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -196,7 +193,7 @@ def test_copy_default_headers(self) -> None: def test_copy_default_query(self) -> None: client = Unlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=True, default_query={"foo": "bar"} + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -321,9 +318,7 @@ def test_request_timeout(self, client: Unlayer) -> None: assert timeout == httpx.Timeout(100.0) def test_client_timeout_option(self) -> None: - client = Unlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=True, timeout=httpx.Timeout(0) - ) + client = Unlayer(base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0)) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -335,7 +330,7 @@ def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: client = Unlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -347,7 +342,7 @@ def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: client = Unlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -359,7 +354,7 @@ def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = Unlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -373,17 +368,14 @@ async def test_invalid_http_client(self) -> None: async with httpx.AsyncClient() as http_client: Unlayer( base_url=base_url, - access_token=access_token, + api_key=api_key, _strict_response_validation=True, http_client=cast(Any, http_client), ) def test_default_headers_option(self) -> None: test_client = Unlayer( - base_url=base_url, - access_token=access_token, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" @@ -391,7 +383,7 @@ def test_default_headers_option(self) -> None: test_client2 = Unlayer( base_url=base_url, - access_token=access_token, + api_key=api_key, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -406,21 +398,27 @@ def test_default_headers_option(self) -> None: test_client2.close() def test_validate_headers(self) -> None: - client = Unlayer(base_url=base_url, access_token=access_token, _strict_response_validation=True) + client = Unlayer(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - assert request.headers.get("Authorization") == f"Bearer {access_token}" + assert request.headers.get("Authorization") == f"Bearer {api_key}" + + with update_env(**{"UNLAYER_API_KEY": Omit()}): + client2 = Unlayer(base_url=base_url, api_key=None, _strict_response_validation=True) - with pytest.raises(UnlayerError): - with update_env(**{"UNLAYER_ACCESS_TOKEN": Omit()}): - client2 = Unlayer(base_url=base_url, access_token=None, _strict_response_validation=True) - _ = client2 + with pytest.raises( + TypeError, + match="Could not resolve authentication method. Expected either api_key or personal_access_token to be set. Or for one of the `Authorization` or `Authorization` headers to be explicitly omitted", + ): + client2._build_request(FinalRequestOptions(method="get", url="/foo")) + + request2 = client2._build_request( + FinalRequestOptions(method="get", url="/foo", headers={"Authorization": Omit()}) + ) + assert request2.headers.get("Authorization") is None def test_default_query_option(self) -> None: client = Unlayer( - base_url=base_url, - access_token=access_token, - _strict_response_validation=True, - default_query={"query_param": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) @@ -592,7 +590,7 @@ def mock_handler(request: httpx.Request) -> httpx.Response: with Unlayer( base_url=base_url, - access_token=access_token, + api_key=api_key, _strict_response_validation=True, http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), ) as client: @@ -686,9 +684,7 @@ class Model(BaseModel): assert response.foo == 2 def test_base_url_setter(self) -> None: - client = Unlayer( - base_url="https://example.com/from_init", access_token=access_token, _strict_response_validation=True - ) + client = Unlayer(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True) assert client.base_url == "https://example.com/from_init/" client.base_url = "https://example.com/from_setter" # type: ignore[assignment] @@ -699,32 +695,16 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(UNLAYER_BASE_URL="http://localhost:5000/from/env"): - client = Unlayer(access_token=access_token, _strict_response_validation=True) + client = Unlayer(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" - # explicit environment arg requires explicitness - with update_env(UNLAYER_BASE_URL="http://localhost:5000/from/env"): - with pytest.raises(ValueError, match=r"you must pass base_url=None"): - Unlayer(access_token=access_token, _strict_response_validation=True, environment="production") - - client = Unlayer( - base_url=None, access_token=access_token, _strict_response_validation=True, environment="production" - ) - assert str(client.base_url).startswith("https://api.unlayer.com") - - client.close() - @pytest.mark.parametrize( "client", [ + Unlayer(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), Unlayer( base_url="http://localhost:5000/custom/path/", - access_token=access_token, - _strict_response_validation=True, - ), - Unlayer( - base_url="http://localhost:5000/custom/path/", - access_token=access_token, + api_key=api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -745,14 +725,10 @@ def test_base_url_trailing_slash(self, client: Unlayer) -> None: @pytest.mark.parametrize( "client", [ + Unlayer(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), Unlayer( base_url="http://localhost:5000/custom/path/", - access_token=access_token, - _strict_response_validation=True, - ), - Unlayer( - base_url="http://localhost:5000/custom/path/", - access_token=access_token, + api_key=api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -773,14 +749,10 @@ def test_base_url_no_trailing_slash(self, client: Unlayer) -> None: @pytest.mark.parametrize( "client", [ + Unlayer(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), Unlayer( base_url="http://localhost:5000/custom/path/", - access_token=access_token, - _strict_response_validation=True, - ), - Unlayer( - base_url="http://localhost:5000/custom/path/", - access_token=access_token, + api_key=api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -799,7 +771,7 @@ def test_absolute_request_url(self, client: Unlayer) -> None: client.close() def test_copied_client_does_not_close_http(self) -> None: - test_client = Unlayer(base_url=base_url, access_token=access_token, _strict_response_validation=True) + test_client = Unlayer(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not test_client.is_closed() copied = test_client.copy() @@ -810,7 +782,7 @@ def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() def test_client_context_manager(self) -> None: - test_client = Unlayer(base_url=base_url, access_token=access_token, _strict_response_validation=True) + test_client = Unlayer(base_url=base_url, api_key=api_key, _strict_response_validation=True) with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -831,12 +803,7 @@ class Model(BaseModel): def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - Unlayer( - base_url=base_url, - access_token=access_token, - _strict_response_validation=True, - max_retries=cast(Any, None), - ) + Unlayer(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) @pytest.mark.respx(base_url=base_url) def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: @@ -845,12 +812,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = Unlayer(base_url=base_url, access_token=access_token, _strict_response_validation=True) + strict_client = Unlayer(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - non_strict_client = Unlayer(base_url=base_url, access_token=access_token, _strict_response_validation=False) + non_strict_client = Unlayer(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -894,7 +861,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.get("/v3/project").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.project.with_streaming_response.retrieve(project_id="projectId").__enter__() + client.project.with_streaming_response.retrieve().__enter__() assert _get_open_connections(client) == 0 @@ -904,7 +871,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.get("/v3/project").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.project.with_streaming_response.retrieve(project_id="projectId").__enter__() + client.project.with_streaming_response.retrieve().__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -933,7 +900,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.get("/v3/project").mock(side_effect=retry_handler) - response = client.project.with_raw_response.retrieve(project_id="projectId") + response = client.project.with_raw_response.retrieve() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -957,9 +924,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.get("/v3/project").mock(side_effect=retry_handler) - response = client.project.with_raw_response.retrieve( - project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} - ) + response = client.project.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -982,9 +947,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.get("/v3/project").mock(side_effect=retry_handler) - response = client.project.with_raw_response.retrieve( - project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} - ) + response = client.project.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1061,9 +1024,9 @@ def test_copy(self, async_client: AsyncUnlayer) -> None: copied = async_client.copy() assert id(copied) != id(async_client) - copied = async_client.copy(access_token="another My Access Token") - assert copied.access_token == "another My Access Token" - assert async_client.access_token == "My Access Token" + copied = async_client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert async_client.api_key == "My API Key" def test_copy_default_options(self, async_client: AsyncUnlayer) -> None: # options that have a default are overridden correctly @@ -1083,10 +1046,7 @@ def test_copy_default_options(self, async_client: AsyncUnlayer) -> None: async def test_copy_default_headers(self) -> None: client = AsyncUnlayer( - base_url=base_url, - access_token=access_token, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -1121,7 +1081,7 @@ async def test_copy_default_headers(self) -> None: async def test_copy_default_query(self) -> None: client = AsyncUnlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=True, default_query={"foo": "bar"} + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -1249,7 +1209,7 @@ async def test_request_timeout(self, async_client: AsyncUnlayer) -> None: async def test_client_timeout_option(self) -> None: client = AsyncUnlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=True, timeout=httpx.Timeout(0) + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1262,7 +1222,7 @@ async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: client = AsyncUnlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1274,7 +1234,7 @@ async def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: client = AsyncUnlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1286,7 +1246,7 @@ async def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = AsyncUnlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1300,17 +1260,14 @@ def test_invalid_http_client(self) -> None: with httpx.Client() as http_client: AsyncUnlayer( base_url=base_url, - access_token=access_token, + api_key=api_key, _strict_response_validation=True, http_client=cast(Any, http_client), ) async def test_default_headers_option(self) -> None: test_client = AsyncUnlayer( - base_url=base_url, - access_token=access_token, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" @@ -1318,7 +1275,7 @@ async def test_default_headers_option(self) -> None: test_client2 = AsyncUnlayer( base_url=base_url, - access_token=access_token, + api_key=api_key, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -1333,21 +1290,27 @@ async def test_default_headers_option(self) -> None: await test_client2.close() def test_validate_headers(self) -> None: - client = AsyncUnlayer(base_url=base_url, access_token=access_token, _strict_response_validation=True) + client = AsyncUnlayer(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - assert request.headers.get("Authorization") == f"Bearer {access_token}" + assert request.headers.get("Authorization") == f"Bearer {api_key}" - with pytest.raises(UnlayerError): - with update_env(**{"UNLAYER_ACCESS_TOKEN": Omit()}): - client2 = AsyncUnlayer(base_url=base_url, access_token=None, _strict_response_validation=True) - _ = client2 + with update_env(**{"UNLAYER_API_KEY": Omit()}): + client2 = AsyncUnlayer(base_url=base_url, api_key=None, _strict_response_validation=True) + + with pytest.raises( + TypeError, + match="Could not resolve authentication method. Expected either api_key or personal_access_token to be set. Or for one of the `Authorization` or `Authorization` headers to be explicitly omitted", + ): + client2._build_request(FinalRequestOptions(method="get", url="/foo")) + + request2 = client2._build_request( + FinalRequestOptions(method="get", url="/foo", headers={"Authorization": Omit()}) + ) + assert request2.headers.get("Authorization") is None async def test_default_query_option(self) -> None: client = AsyncUnlayer( - base_url=base_url, - access_token=access_token, - _strict_response_validation=True, - default_query={"query_param": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) @@ -1519,7 +1482,7 @@ async def mock_handler(request: httpx.Request) -> httpx.Response: async with AsyncUnlayer( base_url=base_url, - access_token=access_token, + api_key=api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), ) as client: @@ -1618,7 +1581,7 @@ class Model(BaseModel): async def test_base_url_setter(self) -> None: client = AsyncUnlayer( - base_url="https://example.com/from_init", access_token=access_token, _strict_response_validation=True + base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) assert client.base_url == "https://example.com/from_init/" @@ -1630,32 +1593,18 @@ async def test_base_url_setter(self) -> None: async def test_base_url_env(self) -> None: with update_env(UNLAYER_BASE_URL="http://localhost:5000/from/env"): - client = AsyncUnlayer(access_token=access_token, _strict_response_validation=True) + client = AsyncUnlayer(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" - # explicit environment arg requires explicitness - with update_env(UNLAYER_BASE_URL="http://localhost:5000/from/env"): - with pytest.raises(ValueError, match=r"you must pass base_url=None"): - AsyncUnlayer(access_token=access_token, _strict_response_validation=True, environment="production") - - client = AsyncUnlayer( - base_url=None, access_token=access_token, _strict_response_validation=True, environment="production" - ) - assert str(client.base_url).startswith("https://api.unlayer.com") - - await client.close() - @pytest.mark.parametrize( "client", [ AsyncUnlayer( - base_url="http://localhost:5000/custom/path/", - access_token=access_token, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), AsyncUnlayer( base_url="http://localhost:5000/custom/path/", - access_token=access_token, + api_key=api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1677,13 +1626,11 @@ async def test_base_url_trailing_slash(self, client: AsyncUnlayer) -> None: "client", [ AsyncUnlayer( - base_url="http://localhost:5000/custom/path/", - access_token=access_token, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), AsyncUnlayer( base_url="http://localhost:5000/custom/path/", - access_token=access_token, + api_key=api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1705,13 +1652,11 @@ async def test_base_url_no_trailing_slash(self, client: AsyncUnlayer) -> None: "client", [ AsyncUnlayer( - base_url="http://localhost:5000/custom/path/", - access_token=access_token, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), AsyncUnlayer( base_url="http://localhost:5000/custom/path/", - access_token=access_token, + api_key=api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1730,7 +1675,7 @@ async def test_absolute_request_url(self, client: AsyncUnlayer) -> None: await client.close() async def test_copied_client_does_not_close_http(self) -> None: - test_client = AsyncUnlayer(base_url=base_url, access_token=access_token, _strict_response_validation=True) + test_client = AsyncUnlayer(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not test_client.is_closed() copied = test_client.copy() @@ -1742,7 +1687,7 @@ async def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - test_client = AsyncUnlayer(base_url=base_url, access_token=access_token, _strict_response_validation=True) + test_client = AsyncUnlayer(base_url=base_url, api_key=api_key, _strict_response_validation=True) async with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -1764,10 +1709,7 @@ class Model(BaseModel): async def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): AsyncUnlayer( - base_url=base_url, - access_token=access_token, - _strict_response_validation=True, - max_retries=cast(Any, None), + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) ) @pytest.mark.respx(base_url=base_url) @@ -1777,14 +1719,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = AsyncUnlayer(base_url=base_url, access_token=access_token, _strict_response_validation=True) + strict_client = AsyncUnlayer(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - non_strict_client = AsyncUnlayer( - base_url=base_url, access_token=access_token, _strict_response_validation=False - ) + non_strict_client = AsyncUnlayer(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = await non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -1830,7 +1770,7 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.get("/v3/project").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.project.with_streaming_response.retrieve(project_id="projectId").__aenter__() + await async_client.project.with_streaming_response.retrieve().__aenter__() assert _get_open_connections(async_client) == 0 @@ -1840,7 +1780,7 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, respx_mock.get("/v3/project").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.project.with_streaming_response.retrieve(project_id="projectId").__aenter__() + await async_client.project.with_streaming_response.retrieve().__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1869,7 +1809,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.get("/v3/project").mock(side_effect=retry_handler) - response = await client.project.with_raw_response.retrieve(project_id="projectId") + response = await client.project.with_raw_response.retrieve() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1893,9 +1833,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.get("/v3/project").mock(side_effect=retry_handler) - response = await client.project.with_raw_response.retrieve( - project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} - ) + response = await client.project.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1918,9 +1856,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.get("/v3/project").mock(side_effect=retry_handler) - response = await client.project.with_raw_response.retrieve( - project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} - ) + response = await client.project.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From e24735806fc790506d1d3b9ee84954809bd02258 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:01:43 +0000 Subject: [PATCH 08/13] feat(api): api update --- .stats.yml | 6 +- README.md | 34 ++++--- api.md | 4 +- src/unlayer/_client.py | 40 ++++---- src/unlayer/resources/__init__.py | 26 +++--- .../resources/{project.py => projects.py} | 92 ++++++++----------- src/unlayer/types/__init__.py | 1 - src/unlayer/types/project_retrieve_params.py | 14 --- .../{test_project.py => test_projects.py} | 52 +++++++---- tests/test_client.py | 74 +++------------ 10 files changed, 144 insertions(+), 199 deletions(-) rename src/unlayer/resources/{project.py => projects.py} (60%) delete mode 100644 src/unlayer/types/project_retrieve_params.py rename tests/api_resources/{test_project.py => test_projects.py} (69%) diff --git a/.stats.yml b/.stats.yml index 2fb1d6b..3004020 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-14707e371ca64ee082f425e10f8bd4b7d9e2eeb28d6e69daad66902abb1b8b6b.yml -openapi_spec_hash: 8198d0442f4736109bf6c49a5a8697ab -config_hash: 144077318a24cfbe9ce6da795a628a80 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-48f00d1c04c23fb4d1cb7cf4af4f56b0c920d758c1f06e06e5373e5b15e9c27d.yml +openapi_spec_hash: 6ee2a94bb9840aceb4a6161c724ce46c +config_hash: c8d97d58d67dad9eeb65eb58fc781724 diff --git a/README.md b/README.md index 2974eba..f4c8537 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,11 @@ client = Unlayer( api_key=os.environ.get("UNLAYER_API_KEY"), # This is the default and can be omitted ) -project = client.project.retrieve( +page = client.templates.list( + limit=10, project_id="your-project-id", ) -print(project.data) +print(page.data) ``` While you can provide an `api_key` keyword argument, @@ -58,10 +59,11 @@ client = AsyncUnlayer( async def main() -> None: - project = await client.project.retrieve( + page = await client.templates.list( + limit=10, project_id="your-project-id", ) - print(project.data) + print(page.data) asyncio.run(main()) @@ -94,10 +96,11 @@ async def main() -> None: api_key=os.environ.get("UNLAYER_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: - project = await client.project.retrieve( + page = await client.templates.list( + limit=10, project_id="your-project-id", ) - print(project.data) + print(page.data) asyncio.run(main()) @@ -218,7 +221,8 @@ from unlayer import Unlayer client = Unlayer() try: - client.project.retrieve( + client.templates.list( + limit=10, project_id="your-project-id", ) except unlayer.APIConnectionError as e: @@ -263,7 +267,8 @@ client = Unlayer( ) # Or, configure per-request: -client.with_options(max_retries=5).project.retrieve( +client.with_options(max_retries=5).templates.list( + limit=10, project_id="your-project-id", ) ``` @@ -288,7 +293,8 @@ client = Unlayer( ) # Override per-request: -client.with_options(timeout=5.0).project.retrieve( +client.with_options(timeout=5.0).templates.list( + limit=10, project_id="your-project-id", ) ``` @@ -331,13 +337,14 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from unlayer import Unlayer client = Unlayer() -response = client.project.with_raw_response.retrieve( +response = client.templates.with_raw_response.list( + limit=10, project_id="your-project-id", ) print(response.headers.get('X-My-Header')) -project = response.parse() # get the object that `project.retrieve()` would have returned -print(project.data) +template = response.parse() # get the object that `templates.list()` would have returned +print(template.id) ``` These methods return an [`APIResponse`](https://github.com/unlayer/unlayer-python/tree/main/src/unlayer/_response.py) object. @@ -351,7 +358,8 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.project.with_streaming_response.retrieve( +with client.templates.with_streaming_response.list( + limit=10, project_id="your-project-id", ) as response: print(response.headers.get("X-My-Header")) diff --git a/api.md b/api.md index 54f0d00..4166417 100644 --- a/api.md +++ b/api.md @@ -24,7 +24,7 @@ Methods: - client.convert.simple_to_full.create(\*\*params) -> SimpleToFullCreateResponse -# Project +# Projects Types: @@ -34,7 +34,7 @@ from unlayer.types import ProjectRetrieveResponse Methods: -- client.project.retrieve(\*\*params) -> ProjectRetrieveResponse +- client.projects.retrieve(id) -> ProjectRetrieveResponse # Templates diff --git a/src/unlayer/_client.py b/src/unlayer/_client.py index 4c34931..08f7fef 100644 --- a/src/unlayer/_client.py +++ b/src/unlayer/_client.py @@ -32,8 +32,8 @@ ) if TYPE_CHECKING: - from .resources import convert, project, templates, workspaces - from .resources.project import ProjectResource, AsyncProjectResource + from .resources import convert, projects, templates, workspaces + from .resources.projects import ProjectsResource, AsyncProjectsResource from .resources.templates import TemplatesResource, AsyncTemplatesResource from .resources.workspaces import WorkspacesResource, AsyncWorkspacesResource from .resources.convert.convert import ConvertResource, AsyncConvertResource @@ -114,10 +114,10 @@ def convert(self) -> ConvertResource: return ConvertResource(self) @cached_property - def project(self) -> ProjectResource: - from .resources.project import ProjectResource + def projects(self) -> ProjectsResource: + from .resources.projects import ProjectsResource - return ProjectResource(self) + return ProjectsResource(self) @cached_property def templates(self) -> TemplatesResource: @@ -344,10 +344,10 @@ def convert(self) -> AsyncConvertResource: return AsyncConvertResource(self) @cached_property - def project(self) -> AsyncProjectResource: - from .resources.project import AsyncProjectResource + def projects(self) -> AsyncProjectsResource: + from .resources.projects import AsyncProjectsResource - return AsyncProjectResource(self) + return AsyncProjectsResource(self) @cached_property def templates(self) -> AsyncTemplatesResource: @@ -514,10 +514,10 @@ def convert(self) -> convert.ConvertResourceWithRawResponse: return ConvertResourceWithRawResponse(self._client.convert) @cached_property - def project(self) -> project.ProjectResourceWithRawResponse: - from .resources.project import ProjectResourceWithRawResponse + def projects(self) -> projects.ProjectsResourceWithRawResponse: + from .resources.projects import ProjectsResourceWithRawResponse - return ProjectResourceWithRawResponse(self._client.project) + return ProjectsResourceWithRawResponse(self._client.projects) @cached_property def templates(self) -> templates.TemplatesResourceWithRawResponse: @@ -545,10 +545,10 @@ def convert(self) -> convert.AsyncConvertResourceWithRawResponse: return AsyncConvertResourceWithRawResponse(self._client.convert) @cached_property - def project(self) -> project.AsyncProjectResourceWithRawResponse: - from .resources.project import AsyncProjectResourceWithRawResponse + def projects(self) -> projects.AsyncProjectsResourceWithRawResponse: + from .resources.projects import AsyncProjectsResourceWithRawResponse - return AsyncProjectResourceWithRawResponse(self._client.project) + return AsyncProjectsResourceWithRawResponse(self._client.projects) @cached_property def templates(self) -> templates.AsyncTemplatesResourceWithRawResponse: @@ -576,10 +576,10 @@ def convert(self) -> convert.ConvertResourceWithStreamingResponse: return ConvertResourceWithStreamingResponse(self._client.convert) @cached_property - def project(self) -> project.ProjectResourceWithStreamingResponse: - from .resources.project import ProjectResourceWithStreamingResponse + def projects(self) -> projects.ProjectsResourceWithStreamingResponse: + from .resources.projects import ProjectsResourceWithStreamingResponse - return ProjectResourceWithStreamingResponse(self._client.project) + return ProjectsResourceWithStreamingResponse(self._client.projects) @cached_property def templates(self) -> templates.TemplatesResourceWithStreamingResponse: @@ -607,10 +607,10 @@ def convert(self) -> convert.AsyncConvertResourceWithStreamingResponse: return AsyncConvertResourceWithStreamingResponse(self._client.convert) @cached_property - def project(self) -> project.AsyncProjectResourceWithStreamingResponse: - from .resources.project import AsyncProjectResourceWithStreamingResponse + def projects(self) -> projects.AsyncProjectsResourceWithStreamingResponse: + from .resources.projects import AsyncProjectsResourceWithStreamingResponse - return AsyncProjectResourceWithStreamingResponse(self._client.project) + return AsyncProjectsResourceWithStreamingResponse(self._client.projects) @cached_property def templates(self) -> templates.AsyncTemplatesResourceWithStreamingResponse: diff --git a/src/unlayer/resources/__init__.py b/src/unlayer/resources/__init__.py index 347e4de..1de5370 100644 --- a/src/unlayer/resources/__init__.py +++ b/src/unlayer/resources/__init__.py @@ -8,13 +8,13 @@ ConvertResourceWithStreamingResponse, AsyncConvertResourceWithStreamingResponse, ) -from .project import ( - ProjectResource, - AsyncProjectResource, - ProjectResourceWithRawResponse, - AsyncProjectResourceWithRawResponse, - ProjectResourceWithStreamingResponse, - AsyncProjectResourceWithStreamingResponse, +from .projects import ( + ProjectsResource, + AsyncProjectsResource, + ProjectsResourceWithRawResponse, + AsyncProjectsResourceWithRawResponse, + ProjectsResourceWithStreamingResponse, + AsyncProjectsResourceWithStreamingResponse, ) from .templates import ( TemplatesResource, @@ -40,12 +40,12 @@ "AsyncConvertResourceWithRawResponse", "ConvertResourceWithStreamingResponse", "AsyncConvertResourceWithStreamingResponse", - "ProjectResource", - "AsyncProjectResource", - "ProjectResourceWithRawResponse", - "AsyncProjectResourceWithRawResponse", - "ProjectResourceWithStreamingResponse", - "AsyncProjectResourceWithStreamingResponse", + "ProjectsResource", + "AsyncProjectsResource", + "ProjectsResourceWithRawResponse", + "AsyncProjectsResourceWithRawResponse", + "ProjectsResourceWithStreamingResponse", + "AsyncProjectsResourceWithStreamingResponse", "TemplatesResource", "AsyncTemplatesResource", "TemplatesResourceWithRawResponse", diff --git a/src/unlayer/resources/project.py b/src/unlayer/resources/projects.py similarity index 60% rename from src/unlayer/resources/project.py rename to src/unlayer/resources/projects.py index af286c2..59073ac 100644 --- a/src/unlayer/resources/project.py +++ b/src/unlayer/resources/projects.py @@ -4,9 +4,7 @@ import httpx -from ..types import project_retrieve_params -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._types import Body, Query, Headers, NotGiven, not_given from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -18,33 +16,33 @@ from .._base_client import make_request_options from ..types.project_retrieve_response import ProjectRetrieveResponse -__all__ = ["ProjectResource", "AsyncProjectResource"] +__all__ = ["ProjectsResource", "AsyncProjectsResource"] -class ProjectResource(SyncAPIResource): +class ProjectsResource(SyncAPIResource): @cached_property - def with_raw_response(self) -> ProjectResourceWithRawResponse: + def with_raw_response(self) -> ProjectsResourceWithRawResponse: """ 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/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ - return ProjectResourceWithRawResponse(self) + return ProjectsResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> ProjectResourceWithStreamingResponse: + def with_streaming_response(self) -> ProjectsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ - return ProjectResourceWithStreamingResponse(self) + return ProjectsResourceWithStreamingResponse(self) def retrieve( self, + id: str, *, - project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -53,11 +51,9 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectRetrieveResponse: """ - Get project details for the specified project. + Get project details by ID. Args: - project_id: The project ID (required for PAT auth, auto-resolved for API key auth) - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -66,43 +62,41 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return self._get( - "/v3/project", + f"/v3/projects/{id}", options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform({"project_id": project_id}, project_retrieve_params.ProjectRetrieveParams), + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=ProjectRetrieveResponse, ) -class AsyncProjectResource(AsyncAPIResource): +class AsyncProjectsResource(AsyncAPIResource): @cached_property - def with_raw_response(self) -> AsyncProjectResourceWithRawResponse: + def with_raw_response(self) -> AsyncProjectsResourceWithRawResponse: """ 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/unlayer/unlayer-python#accessing-raw-response-data-eg-headers """ - return AsyncProjectResourceWithRawResponse(self) + return AsyncProjectsResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> AsyncProjectResourceWithStreamingResponse: + def with_streaming_response(self) -> AsyncProjectsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/unlayer/unlayer-python#with_streaming_response """ - return AsyncProjectResourceWithStreamingResponse(self) + return AsyncProjectsResourceWithStreamingResponse(self) async def retrieve( self, + id: str, *, - project_id: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -111,11 +105,9 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ProjectRetrieveResponse: """ - Get project details for the specified project. + Get project details by ID. Args: - project_id: The project ID (required for PAT auth, auto-resolved for API key auth) - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -124,52 +116,48 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") return await self._get( - "/v3/project", + f"/v3/projects/{id}", options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - {"project_id": project_id}, project_retrieve_params.ProjectRetrieveParams - ), + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=ProjectRetrieveResponse, ) -class ProjectResourceWithRawResponse: - def __init__(self, project: ProjectResource) -> None: - self._project = project +class ProjectsResourceWithRawResponse: + def __init__(self, projects: ProjectsResource) -> None: + self._projects = projects self.retrieve = to_raw_response_wrapper( - project.retrieve, + projects.retrieve, ) -class AsyncProjectResourceWithRawResponse: - def __init__(self, project: AsyncProjectResource) -> None: - self._project = project +class AsyncProjectsResourceWithRawResponse: + def __init__(self, projects: AsyncProjectsResource) -> None: + self._projects = projects self.retrieve = async_to_raw_response_wrapper( - project.retrieve, + projects.retrieve, ) -class ProjectResourceWithStreamingResponse: - def __init__(self, project: ProjectResource) -> None: - self._project = project +class ProjectsResourceWithStreamingResponse: + def __init__(self, projects: ProjectsResource) -> None: + self._projects = projects self.retrieve = to_streamed_response_wrapper( - project.retrieve, + projects.retrieve, ) -class AsyncProjectResourceWithStreamingResponse: - def __init__(self, project: AsyncProjectResource) -> None: - self._project = project +class AsyncProjectsResourceWithStreamingResponse: + def __init__(self, projects: AsyncProjectsResource) -> None: + self._projects = projects self.retrieve = async_to_streamed_response_wrapper( - project.retrieve, + projects.retrieve, ) diff --git a/src/unlayer/types/__init__.py b/src/unlayer/types/__init__.py index 33221d8..3aa8608 100644 --- a/src/unlayer/types/__init__.py +++ b/src/unlayer/types/__init__.py @@ -4,7 +4,6 @@ from .template_list_params import TemplateListParams as TemplateListParams from .template_list_response import TemplateListResponse as TemplateListResponse -from .project_retrieve_params import ProjectRetrieveParams as ProjectRetrieveParams from .workspace_list_response import WorkspaceListResponse as WorkspaceListResponse from .template_retrieve_params import TemplateRetrieveParams as TemplateRetrieveParams from .project_retrieve_response import ProjectRetrieveResponse as ProjectRetrieveResponse diff --git a/src/unlayer/types/project_retrieve_params.py b/src/unlayer/types/project_retrieve_params.py deleted file mode 100644 index f18748a..0000000 --- a/src/unlayer/types/project_retrieve_params.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["ProjectRetrieveParams"] - - -class ProjectRetrieveParams(TypedDict, total=False): - project_id: Annotated[str, PropertyInfo(alias="projectId")] - """The project ID (required for PAT auth, auto-resolved for API key auth)""" diff --git a/tests/api_resources/test_project.py b/tests/api_resources/test_projects.py similarity index 69% rename from tests/api_resources/test_project.py rename to tests/api_resources/test_projects.py index 58151bf..3b4276d 100644 --- a/tests/api_resources/test_project.py +++ b/tests/api_resources/test_projects.py @@ -14,24 +14,21 @@ base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -class TestProject: +class TestProjects: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize def test_method_retrieve(self, client: Unlayer) -> None: - project = client.project.retrieve() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) - - @parametrize - def test_method_retrieve_with_all_params(self, client: Unlayer) -> None: - project = client.project.retrieve( - project_id="projectId", + project = client.projects.retrieve( + "id", ) assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize def test_raw_response_retrieve(self, client: Unlayer) -> None: - response = client.project.with_raw_response.retrieve() + response = client.projects.with_raw_response.retrieve( + "id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -40,7 +37,9 @@ def test_raw_response_retrieve(self, client: Unlayer) -> None: @parametrize def test_streaming_response_retrieve(self, client: Unlayer) -> None: - with client.project.with_streaming_response.retrieve() as response: + with client.projects.with_streaming_response.retrieve( + "id", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -49,27 +48,31 @@ def test_streaming_response_retrieve(self, client: Unlayer) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_path_params_retrieve(self, client: Unlayer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.projects.with_raw_response.retrieve( + "", + ) + -class TestAsyncProject: +class TestAsyncProjects: parametrize = pytest.mark.parametrize( "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) @parametrize async def test_method_retrieve(self, async_client: AsyncUnlayer) -> None: - project = await async_client.project.retrieve() - assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) - - @parametrize - async def test_method_retrieve_with_all_params(self, async_client: AsyncUnlayer) -> None: - project = await async_client.project.retrieve( - project_id="projectId", + project = await async_client.projects.retrieve( + "id", ) assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) @parametrize async def test_raw_response_retrieve(self, async_client: AsyncUnlayer) -> None: - response = await async_client.project.with_raw_response.retrieve() + response = await async_client.projects.with_raw_response.retrieve( + "id", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -78,7 +81,9 @@ async def test_raw_response_retrieve(self, async_client: AsyncUnlayer) -> None: @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncUnlayer) -> None: - async with async_client.project.with_streaming_response.retrieve() as response: + async with async_client.projects.with_streaming_response.retrieve( + "id", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -86,3 +91,10 @@ async def test_streaming_response_retrieve(self, async_client: AsyncUnlayer) -> assert_matches_type(ProjectRetrieveResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncUnlayer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.projects.with_raw_response.retrieve( + "", + ) diff --git a/tests/test_client.py b/tests/test_client.py index 93072a2..a045578 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,7 +23,7 @@ from unlayer._types import Omit from unlayer._utils import asyncify from unlayer._models import BaseModel, FinalRequestOptions -from unlayer._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError +from unlayer._exceptions import APIStatusError, APIResponseValidationError from unlayer._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, @@ -103,14 +103,6 @@ async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] yield item -def _get_open_connections(client: Unlayer | AsyncUnlayer) -> int: - transport = client._client._transport - assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) - - pool = transport._pool - return len(pool._requests) - - class TestUnlayer: @pytest.mark.respx(base_url=base_url) def test_raw_response(self, respx_mock: MockRouter, client: Unlayer) -> None: @@ -855,25 +847,6 @@ def test_parse_retry_after_header( calculated = client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - @mock.patch("unlayer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Unlayer) -> None: - respx_mock.get("/v3/project").mock(side_effect=httpx.TimeoutException("Test timeout error")) - - with pytest.raises(APITimeoutError): - client.project.with_streaming_response.retrieve().__enter__() - - assert _get_open_connections(client) == 0 - - @mock.patch("unlayer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Unlayer) -> None: - respx_mock.get("/v3/project").mock(return_value=httpx.Response(500)) - - with pytest.raises(APIStatusError): - client.project.with_streaming_response.retrieve().__enter__() - assert _get_open_connections(client) == 0 - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("unlayer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -898,9 +871,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v3/project").mock(side_effect=retry_handler) + respx_mock.get("/v3/templates").mock(side_effect=retry_handler) - response = client.project.with_raw_response.retrieve() + response = client.templates.with_raw_response.list() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -922,9 +895,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v3/project").mock(side_effect=retry_handler) + respx_mock.get("/v3/templates").mock(side_effect=retry_handler) - response = client.project.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": Omit()}) + response = client.templates.with_raw_response.list(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -945,9 +918,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v3/project").mock(side_effect=retry_handler) + respx_mock.get("/v3/templates").mock(side_effect=retry_handler) - response = client.project.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": "42"}) + response = client.templates.with_raw_response.list(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1762,27 +1735,6 @@ async def test_parse_retry_after_header( calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - @mock.patch("unlayer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - async def test_retrying_timeout_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncUnlayer - ) -> None: - respx_mock.get("/v3/project").mock(side_effect=httpx.TimeoutException("Test timeout error")) - - with pytest.raises(APITimeoutError): - await async_client.project.with_streaming_response.retrieve().__aenter__() - - assert _get_open_connections(async_client) == 0 - - @mock.patch("unlayer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncUnlayer) -> None: - respx_mock.get("/v3/project").mock(return_value=httpx.Response(500)) - - with pytest.raises(APIStatusError): - await async_client.project.with_streaming_response.retrieve().__aenter__() - assert _get_open_connections(async_client) == 0 - @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("unlayer._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -1807,9 +1759,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v3/project").mock(side_effect=retry_handler) + respx_mock.get("/v3/templates").mock(side_effect=retry_handler) - response = await client.project.with_raw_response.retrieve() + response = await client.templates.with_raw_response.list() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1831,9 +1783,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v3/project").mock(side_effect=retry_handler) + respx_mock.get("/v3/templates").mock(side_effect=retry_handler) - response = await client.project.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": Omit()}) + response = await client.templates.with_raw_response.list(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1854,9 +1806,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v3/project").mock(side_effect=retry_handler) + respx_mock.get("/v3/templates").mock(side_effect=retry_handler) - response = await client.project.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": "42"}) + response = await client.templates.with_raw_response.list(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 82150166172718373d5c625fcb2abd63b996d1c4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 04:16:56 +0000 Subject: [PATCH 09/13] chore(internal): add request options to SSE classes --- src/unlayer/_response.py | 3 +++ src/unlayer/_streaming.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/unlayer/_response.py b/src/unlayer/_response.py index 9a45370..c26d661 100644 --- a/src/unlayer/_response.py +++ b/src/unlayer/_response.py @@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/unlayer/_streaming.py b/src/unlayer/_streaming.py index d6a6f4d..097da18 100644 --- a/src/unlayer/_streaming.py +++ b/src/unlayer/_streaming.py @@ -4,7 +4,7 @@ import json import inspect from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable import httpx @@ -13,6 +13,7 @@ if TYPE_CHECKING: from ._client import Unlayer, AsyncUnlayer + from ._models import FinalRequestOptions _T = TypeVar("_T") @@ -22,7 +23,7 @@ class Stream(Generic[_T]): """Provides the core interface to iterate over a synchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEBytesDecoder def __init__( @@ -31,10 +32,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: Unlayer, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() @@ -85,7 +88,7 @@ class AsyncStream(Generic[_T]): """Provides the core interface to iterate over an asynchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEDecoder | SSEBytesDecoder def __init__( @@ -94,10 +97,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncUnlayer, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() From 17a5609325e6661d12910c96b3f41ecde33e5cf1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 04:30:30 +0000 Subject: [PATCH 10/13] chore(internal): make `test_proxy_environment_variables` more resilient --- tests/test_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index a045578..071a7bc 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -927,6 +927,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultHttpxClient() @@ -1819,6 +1821,8 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultAsyncHttpxClient() From 87402cfe459e821e248671d9d2cc6698920622f9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:55:41 +0000 Subject: [PATCH 11/13] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 3004020..3b0ed67 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-48f00d1c04c23fb4d1cb7cf4af4f56b0c920d758c1f06e06e5373e5b15e9c27d.yml openapi_spec_hash: 6ee2a94bb9840aceb4a6161c724ce46c -config_hash: c8d97d58d67dad9eeb65eb58fc781724 +config_hash: e1b17e2707760d0c014601073f354d8b From bdbc5ef1e0f9f9052f478672bc68ab79bd560808 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:56:16 +0000 Subject: [PATCH 12/13] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 3b0ed67..2702d73 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/unlayer%2Funlayer-48f00d1c04c23fb4d1cb7cf4af4f56b0c920d758c1f06e06e5373e5b15e9c27d.yml openapi_spec_hash: 6ee2a94bb9840aceb4a6161c724ce46c -config_hash: e1b17e2707760d0c014601073f354d8b +config_hash: 249869757b6eb98ae3d58f2a47ce21e2 From 08e2cd50488c7e2e54aaa1c51b79291d71a0b8db Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:56:42 +0000 Subject: [PATCH 13/13] release: 0.1.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 53 +++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/unlayer/_version.py | 2 +- 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1332969..3d2ac0b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1" + ".": "0.1.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3935a47 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# Changelog + +## 0.1.0 (2026-02-24) + +Full Changelog: [v0.0.1...v0.1.0](https://github.com/unlayer/unlayer-python/compare/v0.0.1...v0.1.0) + +### Features + +* **api:** api update ([e247358](https://github.com/unlayer/unlayer-python/commit/e24735806fc790506d1d3b9ee84954809bd02258)) +* **api:** api update ([1255c9b](https://github.com/unlayer/unlayer-python/commit/1255c9b3e6bd708eefb21313c022a1018a78fc3e)) +* **api:** api update ([3901e44](https://github.com/unlayer/unlayer-python/commit/3901e44ec76d4bdc1ae8055639d1121965f8e6c6)) +* **api:** api update ([e90df1d](https://github.com/unlayer/unlayer-python/commit/e90df1d06e53f40707ccc73610d797eaa3c51fa5)) +* **api:** api update ([d0bb085](https://github.com/unlayer/unlayer-python/commit/d0bb0855866ba1daf54f700415c01fcf752090f3)) +* **api:** api update ([39253ba](https://github.com/unlayer/unlayer-python/commit/39253ba25f6a5da8e18da9d564af41e0c7e4c0e7)) +* **api:** api update ([b65e14e](https://github.com/unlayer/unlayer-python/commit/b65e14e7a19564d228e7436bae27b35a9ede6693)) +* **api:** api update ([d83034d](https://github.com/unlayer/unlayer-python/commit/d83034df2dbb4dfc3ee96d101bcdce87dc66306f)) +* **api:** api update ([93b319e](https://github.com/unlayer/unlayer-python/commit/93b319e21ac5f484c9d3efa4a51fba4269fe6737)) +* **api:** api update ([dd927cc](https://github.com/unlayer/unlayer-python/commit/dd927cc54abf0b619091f71e91d778da87323043)) +* **api:** api update ([1a2f183](https://github.com/unlayer/unlayer-python/commit/1a2f183e4fd9a9e512a41855aa1b91b4d28d6bdf)) +* **api:** api update ([1cf5d8e](https://github.com/unlayer/unlayer-python/commit/1cf5d8e9a10c2b8684838fa628b1b864bbe74209)) +* **api:** api update ([35876ba](https://github.com/unlayer/unlayer-python/commit/35876ba78ab506c86a36ae8aead33bf3f04549a6)) +* **api:** api update ([eafe372](https://github.com/unlayer/unlayer-python/commit/eafe372ab530d1a516b6f88630060dd1e384a288)) +* **api:** api update ([db50139](https://github.com/unlayer/unlayer-python/commit/db5013964e0c7efdc485708d622c12fde20c9420)) +* **api:** api update ([37dfa82](https://github.com/unlayer/unlayer-python/commit/37dfa8228ed4158855318afb16b50dc71c78b1bb)) +* **api:** api update ([04d7f14](https://github.com/unlayer/unlayer-python/commit/04d7f14ec06f71d777b7624b8898e3594dcb88f0)) +* **api:** api update ([7dbfb1a](https://github.com/unlayer/unlayer-python/commit/7dbfb1aa7fa765b1750f6ed5a733f4cf584c9766)) +* **api:** api update ([51ce8ef](https://github.com/unlayer/unlayer-python/commit/51ce8ef8927b0824ccd847e0acf61cd57388ae0f)) +* **client:** add custom JSON encoder for extended type support ([16d4717](https://github.com/unlayer/unlayer-python/commit/16d4717e1dec685c212a6c7d74e969660cf7e03c)) +* **client:** add support for binary request streaming ([191bd18](https://github.com/unlayer/unlayer-python/commit/191bd18a0c2588e3742b49b2bb85c23f31c18367)) + + +### Bug Fixes + +* use async_to_httpx_files in patch method ([f9bdb58](https://github.com/unlayer/unlayer-python/commit/f9bdb58b5c218d7310e8c3328fc79d2322bc7d24)) + + +### Chores + +* **ci:** upgrade `actions/github-script` ([89356cd](https://github.com/unlayer/unlayer-python/commit/89356cd10af2df4ce00e52b3857e772c2ffdda9f)) +* configure new SDK language ([9596a35](https://github.com/unlayer/unlayer-python/commit/9596a35093e7c1a7bfb9c1a9da7319482ed4f9c8)) +* format all `api.md` files ([eaab341](https://github.com/unlayer/unlayer-python/commit/eaab341586836de0b5d88ccd15ba91bf5d9fb8a9)) +* **internal:** add `--fix` argument to lint script ([3402766](https://github.com/unlayer/unlayer-python/commit/340276634acd12227076b5b3fbc3a1d5a8aaaf2f)) +* **internal:** add missing files argument to base client ([011c5b3](https://github.com/unlayer/unlayer-python/commit/011c5b3e1d285ad929aa23fbd18f22ded6e37bc2)) +* **internal:** add request options to SSE classes ([8215016](https://github.com/unlayer/unlayer-python/commit/82150166172718373d5c625fcb2abd63b996d1c4)) +* **internal:** bump dependencies ([4796143](https://github.com/unlayer/unlayer-python/commit/47961438c81e3263207987628d1f00c04a117189)) +* **internal:** codegen related update ([d4e8db7](https://github.com/unlayer/unlayer-python/commit/d4e8db7de852054c9232097b80832e630383da0e)) +* **internal:** fix lint error on Python 3.14 ([e825b41](https://github.com/unlayer/unlayer-python/commit/e825b41fa160ab18110a59b28ffdf3c504d63407)) +* **internal:** make `test_proxy_environment_variables` more resilient ([17a5609](https://github.com/unlayer/unlayer-python/commit/17a5609325e6661d12910c96b3f41ecde33e5cf1)) +* **internal:** update `actions/checkout` version ([942160b](https://github.com/unlayer/unlayer-python/commit/942160b8a331a9d21fe2349fba265fd6fb520034)) +* speedup initial import ([a0429e9](https://github.com/unlayer/unlayer-python/commit/a0429e971dcf003df2bbfd2d523bea4aab46db76)) +* update mock server docs ([e835dc3](https://github.com/unlayer/unlayer-python/commit/e835dc37eab896b138333888f423ba0aae6f0958)) +* update SDK settings ([52e5d05](https://github.com/unlayer/unlayer-python/commit/52e5d054a25723d3798cd8a571cd48bc7625bce9)) +* update SDK settings ([63ab6f7](https://github.com/unlayer/unlayer-python/commit/63ab6f7d4fee33593fe9d3dfb6daa615bc00897e)) diff --git a/pyproject.toml b/pyproject.toml index 2bcff9e..cbb8b33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "unlayer" -version = "0.0.1" +version = "0.1.0" description = "The official Python library for the unlayer API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/unlayer/_version.py b/src/unlayer/_version.py index b9adb5b..47398ca 100644 --- a/src/unlayer/_version.py +++ b/src/unlayer/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "unlayer" -__version__ = "0.0.1" # x-release-please-version +__version__ = "0.1.0" # x-release-please-version