From 97ba837736a1793f98958ed8f1c77e89b8c6f322 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Tue, 31 Mar 2026 22:55:06 +0000 Subject: [PATCH 01/15] feat(python-sdk): support optional username/password in basic auth when configured in IR Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/python/sdk/versions.yml | 13 +- .../client_wrapper_generator.py | 57 +- ...e_errors_UnauthorizedRequestErrorBody.json | 13 + .../basic-auth-optional/.fern/metadata.json | 7 + .../.github/workflows/ci.yml | 65 ++ .../python-sdk/basic-auth-optional/.gitignore | 5 + seed/python-sdk/basic-auth-optional/README.md | 168 ++++ .../basic-auth-optional/poetry.lock | 646 ++++++++++++++ .../basic-auth-optional/pyproject.toml | 92 ++ .../basic-auth-optional/reference.md | 138 +++ .../basic-auth-optional/requirements.txt | 4 + .../basic-auth-optional/snippet.json | 31 + .../basic-auth-optional/src/seed/__init__.py | 55 ++ .../src/seed/basic_auth/__init__.py | 4 + .../src/seed/basic_auth/client.py | 178 ++++ .../src/seed/basic_auth/raw_client.py | 238 +++++ .../basic-auth-optional/src/seed/client.py | 164 ++++ .../src/seed/core/__init__.py | 125 +++ .../src/seed/core/api_error.py | 23 + .../src/seed/core/client_wrapper.py | 120 +++ .../src/seed/core/datetime_utils.py | 70 ++ .../basic-auth-optional/src/seed/core/file.py | 67 ++ .../src/seed/core/force_multipart.py | 18 + .../src/seed/core/http_client.py | 840 ++++++++++++++++++ .../src/seed/core/http_response.py | 59 ++ .../src/seed/core/http_sse/__init__.py | 42 + .../src/seed/core/http_sse/_api.py | 112 +++ .../src/seed/core/http_sse/_decoders.py | 61 ++ .../src/seed/core/http_sse/_exceptions.py | 7 + .../src/seed/core/http_sse/_models.py | 17 + .../src/seed/core/jsonable_encoder.py | 108 +++ .../src/seed/core/logging.py | 107 +++ .../src/seed/core/parse_error.py | 36 + .../src/seed/core/pydantic_utilities.py | 634 +++++++++++++ .../src/seed/core/query_encoder.py | 58 ++ .../src/seed/core/remove_none_from_dict.py | 11 + .../src/seed/core/request_options.py | 35 + .../src/seed/core/serialization.py | 276 ++++++ .../src/seed/errors/__init__.py | 39 + .../src/seed/errors/errors/__init__.py | 35 + .../src/seed/errors/errors/bad_request.py | 13 + .../errors/errors/unauthorized_request.py | 11 + .../src/seed/errors/types/__init__.py | 34 + .../types/unauthorized_request_error_body.py | 19 + .../basic-auth-optional/src/seed/py.typed | 0 .../basic-auth-optional/src/seed/version.py | 3 + .../tests/custom/test_client.py | 7 + .../tests/utils/__init__.py | 2 + .../tests/utils/assets/models/__init__.py | 21 + .../tests/utils/assets/models/circle.py | 11 + .../tests/utils/assets/models/color.py | 7 + .../assets/models/object_with_defaults.py | 15 + .../models/object_with_optional_field.py | 35 + .../tests/utils/assets/models/shape.py | 28 + .../tests/utils/assets/models/square.py | 11 + .../assets/models/undiscriminated_shape.py | 10 + .../tests/utils/test_http_client.py | 662 ++++++++++++++ .../tests/utils/test_query_encoding.py | 36 + .../tests/utils/test_serialization.py | 72 ++ .../basic-auth-optional/definition/api.yml | 12 + .../definition/basic-auth.yml | 39 + .../basic-auth-optional/definition/errors.yml | 11 + .../apis/basic-auth-optional/generators.yml | 22 + 63 files changed, 5843 insertions(+), 16 deletions(-) create mode 100644 packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json create mode 100644 seed/python-sdk/basic-auth-optional/.fern/metadata.json create mode 100644 seed/python-sdk/basic-auth-optional/.github/workflows/ci.yml create mode 100644 seed/python-sdk/basic-auth-optional/.gitignore create mode 100644 seed/python-sdk/basic-auth-optional/README.md create mode 100644 seed/python-sdk/basic-auth-optional/poetry.lock create mode 100644 seed/python-sdk/basic-auth-optional/pyproject.toml create mode 100644 seed/python-sdk/basic-auth-optional/reference.md create mode 100644 seed/python-sdk/basic-auth-optional/requirements.txt create mode 100644 seed/python-sdk/basic-auth-optional/snippet.json create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/__init__.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/basic_auth/__init__.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/basic_auth/client.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/basic_auth/raw_client.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/client.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/__init__.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/api_error.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/client_wrapper.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/datetime_utils.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/file.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/force_multipart.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/http_client.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/http_response.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_api.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_models.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/jsonable_encoder.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/logging.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/parse_error.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/pydantic_utilities.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/query_encoder.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/remove_none_from_dict.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/request_options.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/core/serialization.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/errors/__init__.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/errors/errors/__init__.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/errors/errors/bad_request.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/errors/errors/unauthorized_request.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/errors/types/__init__.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/errors/types/unauthorized_request_error_body.py create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/py.typed create mode 100644 seed/python-sdk/basic-auth-optional/src/seed/version.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/custom/test_client.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/__init__.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/assets/models/__init__.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/assets/models/circle.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/assets/models/color.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_defaults.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_optional_field.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/assets/models/shape.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/assets/models/square.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/assets/models/undiscriminated_shape.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/test_http_client.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/test_query_encoding.py create mode 100644 seed/python-sdk/basic-auth-optional/tests/utils/test_serialization.py create mode 100644 test-definitions/fern/apis/basic-auth-optional/definition/api.yml create mode 100644 test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml create mode 100644 test-definitions/fern/apis/basic-auth-optional/definition/errors.yml create mode 100644 test-definitions/fern/apis/basic-auth-optional/generators.yml diff --git a/generators/python/sdk/versions.yml b/generators/python/sdk/versions.yml index 1a6f3aea4c0b..bc2365d92191 100644 --- a/generators/python/sdk/versions.yml +++ b/generators/python/sdk/versions.yml @@ -1,5 +1,17 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json # For unreleased changes, use unreleased.yml +- version: 5.2.1 + changelogEntry: + - summary: | + Support optional username and password in basic auth when configured in IR. + When usernameOmit or passwordOmit is set, the SDK accepts username-only, + password-only, or both credentials. Missing fields are treated as empty strings + (e.g., username-only encodes `username:`, password-only encodes `:password`). + When neither is provided, the Authorization header is omitted entirely. + type: feat + createdAt: "2026-03-31" + irVersion: 65 + - version: 5.2.0 changelogEntry: - summary: | @@ -11,7 +23,6 @@ type: feat createdAt: "2026-03-31" irVersion: 65 - - version: 5.1.3 changelogEntry: - summary: | diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py index 1b22151cb9d2..78d1c5da0e66 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py @@ -523,36 +523,63 @@ def _write_get_headers_body(writer: AST.NodeWriter) -> None: writer.write_newline_if_last_line_not() basic_auth_scheme = self._get_basic_auth_scheme() if basic_auth_scheme is not None: + either_omitted = ( + getattr(basic_auth_scheme, "username_omit", None) is True + or getattr(basic_auth_scheme, "password_omit", None) is True + ) if not self._context.ir.sdk_config.is_auth_mandatory: username_var = names.get_username_constructor_parameter_name(basic_auth_scheme) password_var = names.get_password_constructor_parameter_name(basic_auth_scheme) writer.write_line(f"{username_var} = self.{names.get_username_getter_name(basic_auth_scheme)}()") writer.write_line(f"{password_var} = self.{names.get_password_getter_name(basic_auth_scheme)}()") - writer.write_line(f"if {username_var} is not None and {password_var} is not None:") + condition_op = "or" if either_omitted else "and" + writer.write_line(f"if {username_var} is not None {condition_op} {password_var} is not None:") with writer.indent(): writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') + if either_omitted: + writer.write_node( + AST.ClassInstantiation( + class_=httpx.HttpX.BASIC_AUTH, + args=[ + AST.Expression(f'{username_var} or ""'), + AST.Expression(f'{password_var} or ""'), + ], + ) + ) + else: + writer.write_node( + AST.ClassInstantiation( + class_=httpx.HttpX.BASIC_AUTH, + args=[ + AST.Expression(f"{username_var}"), + AST.Expression(f"{password_var}"), + ], + ) + ) + writer.write("._auth_header") + writer.write_newline_if_last_line_not() + else: + writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') + if either_omitted: writer.write_node( AST.ClassInstantiation( class_=httpx.HttpX.BASIC_AUTH, args=[ - AST.Expression(f"{username_var}"), - AST.Expression(f"{password_var}"), + AST.Expression(f'self.{names.get_username_getter_name(basic_auth_scheme)}() or ""'), + AST.Expression(f'self.{names.get_password_getter_name(basic_auth_scheme)}() or ""'), ], ) ) - writer.write("._auth_header") - writer.write_newline_if_last_line_not() - else: - writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') - writer.write_node( - AST.ClassInstantiation( - class_=httpx.HttpX.BASIC_AUTH, - args=[ - AST.Expression(f"self.{names.get_username_getter_name(basic_auth_scheme)}()"), - AST.Expression(f"self.{names.get_password_getter_name(basic_auth_scheme)}()"), - ], + else: + writer.write_node( + AST.ClassInstantiation( + class_=httpx.HttpX.BASIC_AUTH, + args=[ + AST.Expression(f"self.{names.get_username_getter_name(basic_auth_scheme)}()"), + AST.Expression(f"self.{names.get_password_getter_name(basic_auth_scheme)}()"), + ], + ) ) - ) writer.write("._auth_header") writer.write_newline_if_last_line_not() for param in constructor_parameters: diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json new file mode 100644 index 000000000000..f50ccac10d76 --- /dev/null +++ b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false, + "definitions": {} +} \ No newline at end of file diff --git a/seed/python-sdk/basic-auth-optional/.fern/metadata.json b/seed/python-sdk/basic-auth-optional/.fern/metadata.json new file mode 100644 index 000000000000..a3141989173b --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/.fern/metadata.json @@ -0,0 +1,7 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-python-sdk", + "generatorVersion": "latest", + "originGitCommit": "DUMMY", + "sdkVersion": "0.0.1" +} \ No newline at end of file diff --git a/seed/python-sdk/basic-auth-optional/.github/workflows/ci.yml b/seed/python-sdk/basic-auth-optional/.github/workflows/ci.yml new file mode 100644 index 000000000000..f48e9eb7577d --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: ci +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Compile + run: poetry run mypy . + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + + - name: Test + run: poetry run pytest -rP -n auto . + + publish: + needs: [compile, test] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Publish to pypi + run: | + poetry config repositories.remote + poetry --no-interaction -v publish --build --repository remote --username "$PYPI_USERNAME" --password "$PYPI_PASSWORD" + env: + PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} + PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} diff --git a/seed/python-sdk/basic-auth-optional/.gitignore b/seed/python-sdk/basic-auth-optional/.gitignore new file mode 100644 index 000000000000..d2e4ca808d21 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/.gitignore @@ -0,0 +1,5 @@ +.mypy_cache/ +.ruff_cache/ +__pycache__/ +dist/ +poetry.toml diff --git a/seed/python-sdk/basic-auth-optional/README.md b/seed/python-sdk/basic-auth-optional/README.md new file mode 100644 index 000000000000..9bb342b7a6d3 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/README.md @@ -0,0 +1,168 @@ +# Seed Python Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FPython) +[![pypi](https://img.shields.io/pypi/v/fern_basic-auth-optional)](https://pypi.python.org/pypi/fern_basic-auth-optional) + +The Seed Python library provides convenient access to the Seed APIs from Python. + +## Table of Contents + +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Async Client](#async-client) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Access Raw Response Data](#access-raw-response-data) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Custom Client](#custom-client) +- [Contributing](#contributing) + +## Installation + +```sh +pip install fern_basic-auth-optional +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```python +from seed import SeedBasicAuthOptional + +client = SeedBasicAuthOptional( + username="", + password="", + base_url="https://yourhost.com/path/to/api", +) + +client.basic_auth.post_with_basic_auth( + request={"key": "value"}, +) +``` + +## Async Client + +The SDK also exports an `async` client so that you can make non-blocking calls to our API. Note that if you are constructing an Async httpx client class to pass into this client, use `httpx.AsyncClient()` instead of `httpx.Client()` (e.g. for the `httpx_client` parameter of this client). + +```python +import asyncio + +from seed import AsyncSeedBasicAuthOptional + +client = AsyncSeedBasicAuthOptional( + username="", + password="", + base_url="https://yourhost.com/path/to/api", +) + + +async def main() -> None: + await client.basic_auth.post_with_basic_auth( + request={"key": "value"}, + ) + + +asyncio.run(main()) +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```python +from seed.core.api_error import ApiError + +try: + client.basic_auth.post_with_basic_auth(...) +except ApiError as e: + print(e.status_code) + print(e.body) +``` + +## Advanced + +### Access Raw Response Data + +The SDK provides access to raw response data, including headers, through the `.with_raw_response` property. +The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes. + +```python +from seed import SeedBasicAuthOptional + +client = SeedBasicAuthOptional(...) +response = client.basic_auth.with_raw_response.post_with_basic_auth(...) +print(response.headers) # access the response headers +print(response.status_code) # access the response status code +print(response.data) # access the underlying object +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +A request is deemed retryable when any of the following HTTP status codes is returned: + +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) + +Use the `max_retries` request option to configure this behavior. + +```python +client.basic_auth.post_with_basic_auth(..., request_options={ + "max_retries": 1 +}) +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. + +```python +from seed import SeedBasicAuthOptional + +client = SeedBasicAuthOptional(..., timeout=20.0) + +# Override timeout for a specific method +client.basic_auth.post_with_basic_auth(..., request_options={ + "timeout_in_seconds": 1 +}) +``` + +### Custom Client + +You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies +and transports. + +```python +import httpx +from seed import SeedBasicAuthOptional + +client = SeedBasicAuthOptional( + ..., + httpx_client=httpx.Client( + proxy="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/python-sdk/basic-auth-optional/poetry.lock b/seed/python-sdk/basic-auth-optional/poetry.lock new file mode 100644 index 000000000000..f3487f4eb233 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/poetry.lock @@ -0,0 +1,646 @@ +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.13.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.10" +files = [ + {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, + {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.32.0)"] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." +optional = false +python-versions = "<3.11,>=3.8" +files = [ + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "execnet" +version = "2.1.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, + {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "2.12.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, + {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, +] + +[package.dependencies] +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} +pytest = ">=8.2,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, + {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, +] + +[package.dependencies] +execnet = ">=2.1" +pytest = ">=7.0.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "ruff" +version = "0.11.5" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b"}, + {file = "ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077"}, + {file = "ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783"}, + {file = "ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe"}, + {file = "ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800"}, + {file = "ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e"}, + {file = "ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "tomli" +version = "2.4.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20260323" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.10" +files = [ + {file = "types_python_dateutil-2.9.0.20260323-py3-none-any.whl", hash = "sha256:a23a50a07f6eb87e729d4cb0c2eb511c81761eeb3f505db2c1413be94aae8335"}, + {file = "types_python_dateutil-2.9.0.20260323.tar.gz", hash = "sha256:a107aef5841db41ace381dbbbd7e4945220fc940f7a72172a0be5a92d9ab7164"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "38d3ecf12c1dec83f3d75b7fb34e0360e556c17672ddce7d0b373e7f8afde1ba" diff --git a/seed/python-sdk/basic-auth-optional/pyproject.toml b/seed/python-sdk/basic-auth-optional/pyproject.toml new file mode 100644 index 000000000000..18ff2a085e19 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/pyproject.toml @@ -0,0 +1,92 @@ +[project] +name = "fern_basic-auth-optional" +dynamic = ["version"] + +[tool.poetry] +name = "fern_basic-auth-optional" +version = "0.0.1" +description = "" +readme = "README.md" +authors = [] +keywords = [ + "fern", + "test" +] + +classifiers = [ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed" +] +packages = [ + { include = "seed", from = "src"} +] + +[tool.poetry.urls] +Documentation = 'https://buildwithfern.com/learn' +Homepage = 'https://buildwithfern.com/' +Repository = 'https://github.com/basic-auth-optional/fern' + +[tool.poetry.dependencies] +python = "^3.10" +httpx = ">=0.21.2" +pydantic = ">= 1.9.2" +pydantic-core = ">=2.18.2,<2.44.0" +typing_extensions = ">= 4.0.0" + +[tool.poetry.group.dev.dependencies] +mypy = "==1.13.0" +pytest = "^8.2.0" +pytest-asyncio = "^1.0.0" +pytest-xdist = "^3.6.1" +python-dateutil = "^2.9.0" +types-python-dateutil = "^2.9.0.20240316" +ruff = "==0.11.5" + +[tool.pytest.ini_options] +testpaths = [ "tests" ] +asyncio_mode = "auto" + +[tool.mypy] +plugins = ["pydantic.mypy"] + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort +] +ignore = [ + "E402", # Module level import not at top of file + "E501", # Line too long + "E711", # Comparison to `None` should be `cond is not None` + "E712", # Avoid equality comparisons to `True`; use `if ...:` checks + "E721", # Use `is` and `is not` for type comparisons, or `isinstance()` for insinstance checks + "E722", # Do not use bare `except` + "E731", # Do not assign a `lambda` expression, use a `def` + "F821", # Undefined name + "F841" # Local variable ... is assigned to but never used +] + +[tool.ruff.lint.isort] +section-order = ["future", "standard-library", "third-party", "first-party"] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/seed/python-sdk/basic-auth-optional/reference.md b/seed/python-sdk/basic-auth-optional/reference.md new file mode 100644 index 000000000000..6bda1fddd2f8 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/reference.md @@ -0,0 +1,138 @@ +# Reference +## BasicAuth +
client.basic_auth.get_with_basic_auth() -> bool +
+
+ +#### 📝 Description + +
+
+ +
+
+ +GET request with basic auth scheme +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedBasicAuthOptional + +client = SeedBasicAuthOptional( + username="", + password="", + base_url="https://yourhost.com/path/to/api", +) + +client.basic_auth.get_with_basic_auth() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.basic_auth.post_with_basic_auth(...) -> bool +
+
+ +#### 📝 Description + +
+
+ +
+
+ +POST request with basic auth scheme +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedBasicAuthOptional + +client = SeedBasicAuthOptional( + username="", + password="", + base_url="https://yourhost.com/path/to/api", +) + +client.basic_auth.post_with_basic_auth( + request={"key": "value"}, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `typing.Any` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ diff --git a/seed/python-sdk/basic-auth-optional/requirements.txt b/seed/python-sdk/basic-auth-optional/requirements.txt new file mode 100644 index 000000000000..0141a1a5014b --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/requirements.txt @@ -0,0 +1,4 @@ +httpx>=0.21.2 +pydantic>= 1.9.2 +pydantic-core>=2.18.2,<2.44.0 +typing_extensions>= 4.0.0 diff --git a/seed/python-sdk/basic-auth-optional/snippet.json b/seed/python-sdk/basic-auth-optional/snippet.json new file mode 100644 index 000000000000..2bbf238b62a5 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/snippet.json @@ -0,0 +1,31 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": "default", + "id": { + "path": "/basic-auth", + "method": "GET", + "identifier_override": "endpoint_basic-auth.getWithBasicAuth" + }, + "snippet": { + "sync_client": "from seed import SeedBasicAuthOptional\n\nclient = SeedBasicAuthOptional(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.basic_auth.get_with_basic_auth()\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedBasicAuthOptional\n\nclient = AsyncSeedBasicAuthOptional(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.basic_auth.get_with_basic_auth()\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/basic-auth", + "method": "POST", + "identifier_override": "endpoint_basic-auth.postWithBasicAuth" + }, + "snippet": { + "sync_client": "from seed import SeedBasicAuthOptional\n\nclient = SeedBasicAuthOptional(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.basic_auth.post_with_basic_auth(\n request={\"key\": \"value\"},\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedBasicAuthOptional\n\nclient = AsyncSeedBasicAuthOptional(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.basic_auth.post_with_basic_auth(\n request={\"key\": \"value\"},\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + } + ] +} \ No newline at end of file diff --git a/seed/python-sdk/basic-auth-optional/src/seed/__init__.py b/seed/python-sdk/basic-auth-optional/src/seed/__init__.py new file mode 100644 index 000000000000..a9c6abbc275e --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/__init__.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .errors import BadRequest, UnauthorizedRequest, UnauthorizedRequestErrorBody + from . import basic_auth, errors + from .client import AsyncSeedBasicAuthOptional, SeedBasicAuthOptional + from .version import __version__ +_dynamic_imports: typing.Dict[str, str] = { + "AsyncSeedBasicAuthOptional": ".client", + "BadRequest": ".errors", + "SeedBasicAuthOptional": ".client", + "UnauthorizedRequest": ".errors", + "UnauthorizedRequestErrorBody": ".errors", + "__version__": ".version", + "basic_auth": ".basic_auth", + "errors": ".errors", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AsyncSeedBasicAuthOptional", + "BadRequest", + "SeedBasicAuthOptional", + "UnauthorizedRequest", + "UnauthorizedRequestErrorBody", + "__version__", + "basic_auth", + "errors", +] diff --git a/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/__init__.py b/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/__init__.py new file mode 100644 index 000000000000..5cde0202dcf3 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/client.py b/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/client.py new file mode 100644 index 000000000000..2126381f8570 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/client.py @@ -0,0 +1,178 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawBasicAuthClient, RawBasicAuthClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class BasicAuthClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawBasicAuthClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawBasicAuthClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawBasicAuthClient + """ + return self._raw_client + + def get_with_basic_auth(self, *, request_options: typing.Optional[RequestOptions] = None) -> bool: + """ + GET request with basic auth scheme + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + bool + + Examples + -------- + from seed import SeedBasicAuthOptional + + client = SeedBasicAuthOptional( + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.basic_auth.get_with_basic_auth() + """ + _response = self._raw_client.get_with_basic_auth(request_options=request_options) + return _response.data + + def post_with_basic_auth( + self, *, request: typing.Any, request_options: typing.Optional[RequestOptions] = None + ) -> bool: + """ + POST request with basic auth scheme + + Parameters + ---------- + request : typing.Any + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + bool + + Examples + -------- + from seed import SeedBasicAuthOptional + + client = SeedBasicAuthOptional( + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.basic_auth.post_with_basic_auth( + request={"key": "value"}, + ) + """ + _response = self._raw_client.post_with_basic_auth(request=request, request_options=request_options) + return _response.data + + +class AsyncBasicAuthClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawBasicAuthClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawBasicAuthClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawBasicAuthClient + """ + return self._raw_client + + async def get_with_basic_auth(self, *, request_options: typing.Optional[RequestOptions] = None) -> bool: + """ + GET request with basic auth scheme + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + bool + + Examples + -------- + import asyncio + + from seed import AsyncSeedBasicAuthOptional + + client = AsyncSeedBasicAuthOptional( + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.basic_auth.get_with_basic_auth() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_with_basic_auth(request_options=request_options) + return _response.data + + async def post_with_basic_auth( + self, *, request: typing.Any, request_options: typing.Optional[RequestOptions] = None + ) -> bool: + """ + POST request with basic auth scheme + + Parameters + ---------- + request : typing.Any + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + bool + + Examples + -------- + import asyncio + + from seed import AsyncSeedBasicAuthOptional + + client = AsyncSeedBasicAuthOptional( + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.basic_auth.post_with_basic_auth( + request={"key": "value"}, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.post_with_basic_auth(request=request, request_options=request_options) + return _response.data diff --git a/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/raw_client.py b/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/raw_client.py new file mode 100644 index 000000000000..b5d50e093f6d --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/raw_client.py @@ -0,0 +1,238 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.parse_error import ParsingError +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..errors.errors.bad_request import BadRequest +from ..errors.errors.unauthorized_request import UnauthorizedRequest +from ..errors.types.unauthorized_request_error_body import UnauthorizedRequestErrorBody +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawBasicAuthClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_with_basic_auth(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[bool]: + """ + GET request with basic auth scheme + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[bool] + """ + _response = self._client_wrapper.httpx_client.request( + "basic-auth", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + bool, + parse_obj_as( + type_=bool, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedRequest( + headers=dict(_response.headers), + body=typing.cast( + UnauthorizedRequestErrorBody, + parse_obj_as( + type_=UnauthorizedRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def post_with_basic_auth( + self, *, request: typing.Any, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[bool]: + """ + POST request with basic auth scheme + + Parameters + ---------- + request : typing.Any + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[bool] + """ + _response = self._client_wrapper.httpx_client.request( + "basic-auth", + method="POST", + json=request, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + bool, + parse_obj_as( + type_=bool, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedRequest( + headers=dict(_response.headers), + body=typing.cast( + UnauthorizedRequestErrorBody, + parse_obj_as( + type_=UnauthorizedRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise BadRequest(headers=dict(_response.headers)) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawBasicAuthClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_with_basic_auth( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[bool]: + """ + GET request with basic auth scheme + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[bool] + """ + _response = await self._client_wrapper.httpx_client.request( + "basic-auth", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + bool, + parse_obj_as( + type_=bool, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedRequest( + headers=dict(_response.headers), + body=typing.cast( + UnauthorizedRequestErrorBody, + parse_obj_as( + type_=UnauthorizedRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def post_with_basic_auth( + self, *, request: typing.Any, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[bool]: + """ + POST request with basic auth scheme + + Parameters + ---------- + request : typing.Any + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[bool] + """ + _response = await self._client_wrapper.httpx_client.request( + "basic-auth", + method="POST", + json=request, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + bool, + parse_obj_as( + type_=bool, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedRequest( + headers=dict(_response.headers), + body=typing.cast( + UnauthorizedRequestErrorBody, + parse_obj_as( + type_=UnauthorizedRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 400: + raise BadRequest(headers=dict(_response.headers)) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/seed/python-sdk/basic-auth-optional/src/seed/client.py b/seed/python-sdk/basic-auth-optional/src/seed/client.py new file mode 100644 index 000000000000..a2c698bd1d06 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/client.py @@ -0,0 +1,164 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import httpx +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .core.logging import LogConfig, Logger + +if typing.TYPE_CHECKING: + from .basic_auth.client import AsyncBasicAuthClient, BasicAuthClient + + +class SeedBasicAuthOptional: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + username : typing.Union[str, typing.Callable[[], str]] + password : typing.Union[str, typing.Callable[[], str]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + logging : typing.Optional[typing.Union[LogConfig, Logger]] + Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. + + Examples + -------- + from seed import SeedBasicAuthOptional + + client = SeedBasicAuthOptional( + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + username: typing.Union[str, typing.Callable[[], str]], + password: typing.Union[str, typing.Callable[[], str]], + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = SyncClientWrapper( + base_url=base_url, + username=username, + password=password, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + logging=logging, + ) + self._basic_auth: typing.Optional[BasicAuthClient] = None + + @property + def basic_auth(self): + if self._basic_auth is None: + from .basic_auth.client import BasicAuthClient # noqa: E402 + + self._basic_auth = BasicAuthClient(client_wrapper=self._client_wrapper) + return self._basic_auth + + +class AsyncSeedBasicAuthOptional: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + username : typing.Union[str, typing.Callable[[], str]] + password : typing.Union[str, typing.Callable[[], str]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + logging : typing.Optional[typing.Union[LogConfig, Logger]] + Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. + + Examples + -------- + from seed import AsyncSeedBasicAuthOptional + + client = AsyncSeedBasicAuthOptional( + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + username: typing.Union[str, typing.Callable[[], str]], + password: typing.Union[str, typing.Callable[[], str]], + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = AsyncClientWrapper( + base_url=base_url, + username=username, + password=password, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + logging=logging, + ) + self._basic_auth: typing.Optional[AsyncBasicAuthClient] = None + + @property + def basic_auth(self): + if self._basic_auth is None: + from .basic_auth.client import AsyncBasicAuthClient # noqa: E402 + + self._basic_auth = AsyncBasicAuthClient(client_wrapper=self._client_wrapper) + return self._basic_auth diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/__init__.py b/seed/python-sdk/basic-auth-optional/src/seed/core/__init__.py new file mode 100644 index 000000000000..4fb6e12e0dff --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/__init__.py @@ -0,0 +1,125 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .api_error import ApiError + from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper + from .datetime_utils import Rfc2822DateTime, parse_rfc2822_datetime, serialize_datetime + from .file import File, convert_file_dict_to_httpx_tuples, with_content_type + from .http_client import AsyncHttpClient, HttpClient + from .http_response import AsyncHttpResponse, HttpResponse + from .jsonable_encoder import jsonable_encoder + from .logging import ConsoleLogger, ILogger, LogConfig, LogLevel, Logger, create_logger + from .parse_error import ParsingError + from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, + ) + from .query_encoder import encode_query + from .remove_none_from_dict import remove_none_from_dict + from .request_options import RequestOptions + from .serialization import FieldMetadata, convert_and_respect_annotation_metadata +_dynamic_imports: typing.Dict[str, str] = { + "ApiError": ".api_error", + "AsyncClientWrapper": ".client_wrapper", + "AsyncHttpClient": ".http_client", + "AsyncHttpResponse": ".http_response", + "BaseClientWrapper": ".client_wrapper", + "ConsoleLogger": ".logging", + "FieldMetadata": ".serialization", + "File": ".file", + "HttpClient": ".http_client", + "HttpResponse": ".http_response", + "ILogger": ".logging", + "IS_PYDANTIC_V2": ".pydantic_utilities", + "LogConfig": ".logging", + "LogLevel": ".logging", + "Logger": ".logging", + "ParsingError": ".parse_error", + "RequestOptions": ".request_options", + "Rfc2822DateTime": ".datetime_utils", + "SyncClientWrapper": ".client_wrapper", + "UniversalBaseModel": ".pydantic_utilities", + "UniversalRootModel": ".pydantic_utilities", + "convert_and_respect_annotation_metadata": ".serialization", + "convert_file_dict_to_httpx_tuples": ".file", + "create_logger": ".logging", + "encode_query": ".query_encoder", + "jsonable_encoder": ".jsonable_encoder", + "parse_obj_as": ".pydantic_utilities", + "parse_rfc2822_datetime": ".datetime_utils", + "remove_none_from_dict": ".remove_none_from_dict", + "serialize_datetime": ".datetime_utils", + "universal_field_validator": ".pydantic_utilities", + "universal_root_validator": ".pydantic_utilities", + "update_forward_refs": ".pydantic_utilities", + "with_content_type": ".file", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "ApiError", + "AsyncClientWrapper", + "AsyncHttpClient", + "AsyncHttpResponse", + "BaseClientWrapper", + "ConsoleLogger", + "FieldMetadata", + "File", + "HttpClient", + "HttpResponse", + "ILogger", + "IS_PYDANTIC_V2", + "LogConfig", + "LogLevel", + "Logger", + "ParsingError", + "RequestOptions", + "Rfc2822DateTime", + "SyncClientWrapper", + "UniversalBaseModel", + "UniversalRootModel", + "convert_and_respect_annotation_metadata", + "convert_file_dict_to_httpx_tuples", + "create_logger", + "encode_query", + "jsonable_encoder", + "parse_obj_as", + "parse_rfc2822_datetime", + "remove_none_from_dict", + "serialize_datetime", + "universal_field_validator", + "universal_root_validator", + "update_forward_refs", + "with_content_type", +] diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/api_error.py b/seed/python-sdk/basic-auth-optional/src/seed/core/api_error.py new file mode 100644 index 000000000000..6f850a60cba3 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/api_error.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ApiError(Exception): + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + + def __str__(self) -> str: + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}" diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/client_wrapper.py b/seed/python-sdk/basic-auth-optional/src/seed/core/client_wrapper.py new file mode 100644 index 000000000000..16988c81f98c --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/client_wrapper.py @@ -0,0 +1,120 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .http_client import AsyncHttpClient, HttpClient +from .logging import LogConfig, Logger + + +class BaseClientWrapper: + def __init__( + self, + *, + username: typing.Union[str, typing.Callable[[], str]], + password: typing.Union[str, typing.Callable[[], str]], + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self._username = username + self._password = password + self._headers = headers + self._base_url = base_url + self._timeout = timeout + self._logging = logging + + def get_headers(self) -> typing.Dict[str, str]: + import platform + + headers: typing.Dict[str, str] = { + "User-Agent": "fern_basic-auth-optional/0.0.1", + "X-Fern-Language": "Python", + "X-Fern-Runtime": f"python/{platform.python_version()}", + "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", + "X-Fern-SDK-Name": "fern_basic-auth-optional", + "X-Fern-SDK-Version": "0.0.1", + **(self.get_custom_headers() or {}), + } + headers["Authorization"] = httpx.BasicAuth(self._get_username() or "", self._get_password() or "")._auth_header + return headers + + def _get_username(self) -> str: + if isinstance(self._username, str): + return self._username + else: + return self._username() + + def _get_password(self) -> str: + if isinstance(self._password, str): + return self._password + else: + return self._password() + + def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: + return self._headers + + def get_base_url(self) -> str: + return self._base_url + + def get_timeout(self) -> typing.Optional[float]: + return self._timeout + + +class SyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + username: typing.Union[str, typing.Callable[[], str]], + password: typing.Union[str, typing.Callable[[], str]], + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + httpx_client: httpx.Client, + ): + super().__init__( + username=username, password=password, headers=headers, base_url=base_url, timeout=timeout, logging=logging + ) + self.httpx_client = HttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + logging_config=self._logging, + ) + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + username: typing.Union[str, typing.Callable[[], str]], + password: typing.Union[str, typing.Callable[[], str]], + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, + httpx_client: httpx.AsyncClient, + ): + super().__init__( + username=username, password=password, headers=headers, base_url=base_url, timeout=timeout, logging=logging + ) + self._async_token = async_token + self.httpx_client = AsyncHttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + async_base_headers=self.async_get_headers, + logging_config=self._logging, + ) + + async def async_get_headers(self) -> typing.Dict[str, str]: + headers = self.get_headers() + if self._async_token is not None: + token = await self._async_token() + headers["Authorization"] = f"Bearer {token}" + return headers diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/datetime_utils.py b/seed/python-sdk/basic-auth-optional/src/seed/core/datetime_utils.py new file mode 100644 index 000000000000..a12b2ad03c53 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/datetime_utils.py @@ -0,0 +1,70 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +from email.utils import parsedate_to_datetime +from typing import Any + +import pydantic + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + + +def parse_rfc2822_datetime(v: Any) -> dt.datetime: + """ + Parse an RFC 2822 datetime string (e.g., "Wed, 02 Oct 2002 13:00:00 GMT") + into a datetime object. If the value is already a datetime, return it as-is. + Falls back to ISO 8601 parsing if RFC 2822 parsing fails. + """ + if isinstance(v, dt.datetime): + return v + if isinstance(v, str): + try: + return parsedate_to_datetime(v) + except Exception: + pass + # Fallback to ISO 8601 parsing + return dt.datetime.fromisoformat(v.replace("Z", "+00:00")) + raise ValueError(f"Expected str or datetime, got {type(v)}") + + +class Rfc2822DateTime(dt.datetime): + """A datetime subclass that parses RFC 2822 date strings. + + On Pydantic V1, uses __get_validators__ for pre-validation. + On Pydantic V2, uses __get_pydantic_core_schema__ for BeforeValidator-style parsing. + """ + + @classmethod + def __get_validators__(cls): # type: ignore[no-untyped-def] + yield parse_rfc2822_datetime + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any: # type: ignore[override] + from pydantic_core import core_schema + + return core_schema.no_info_before_validator_function(parse_rfc2822_datetime, core_schema.datetime_schema()) + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/file.py b/seed/python-sdk/basic-auth-optional/src/seed/core/file.py new file mode 100644 index 000000000000..44b0d27c0895 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/file.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import IO, Dict, List, Mapping, Optional, Tuple, Union, cast + +# File typing inspired by the flexibility of types within the httpx library +# https://github.com/encode/httpx/blob/master/httpx/_types.py +FileContent = Union[IO[bytes], bytes, str] +File = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[ + Optional[str], + FileContent, + Optional[str], + Mapping[str, str], + ], +] + + +def convert_file_dict_to_httpx_tuples( + d: Dict[str, Union[File, List[File]]], +) -> List[Tuple[str, File]]: + """ + The format we use is a list of tuples, where the first element is the + name of the file and the second is the file object. Typically HTTPX wants + a dict, but to be able to send lists of files, you have to use the list + approach (which also works for non-lists) + https://github.com/encode/httpx/pull/1032 + """ + + httpx_tuples = [] + for key, file_like in d.items(): + if isinstance(file_like, list): + for file_like_item in file_like: + httpx_tuples.append((key, file_like_item)) + else: + httpx_tuples.append((key, file_like)) + return httpx_tuples + + +def with_content_type(*, file: File, default_content_type: str) -> File: + """ + This function resolves to the file's content type, if provided, and defaults + to the default_content_type value if not. + """ + if isinstance(file, tuple): + if len(file) == 2: + filename, content = cast(Tuple[Optional[str], FileContent], file) # type: ignore + return (filename, content, default_content_type) + elif len(file) == 3: + filename, content, file_content_type = cast(Tuple[Optional[str], FileContent, Optional[str]], file) # type: ignore + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type) + elif len(file) == 4: + filename, content, file_content_type, headers = cast( # type: ignore + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], file + ) + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type, headers) + else: + raise ValueError(f"Unexpected tuple length: {len(file)}") + return (None, file, default_content_type) diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/force_multipart.py b/seed/python-sdk/basic-auth-optional/src/seed/core/force_multipart.py new file mode 100644 index 000000000000..5440913fd4bc --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/force_multipart.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict + + +class ForceMultipartDict(Dict[str, Any]): + """ + A dictionary subclass that always evaluates to True in boolean contexts. + + This is used to force multipart/form-data encoding in HTTP requests even when + the dictionary is empty, which would normally evaluate to False. + """ + + def __bool__(self) -> bool: + return True + + +FORCE_MULTIPART = ForceMultipartDict() diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_client.py b/seed/python-sdk/basic-auth-optional/src/seed/core/http_client.py new file mode 100644 index 000000000000..f0a39ca8243a --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/http_client.py @@ -0,0 +1,840 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import email.utils +import re +import time +import typing +from contextlib import asynccontextmanager, contextmanager +from random import random + +import httpx +from .file import File, convert_file_dict_to_httpx_tuples +from .force_multipart import FORCE_MULTIPART +from .jsonable_encoder import jsonable_encoder +from .logging import LogConfig, Logger, create_logger +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict +from .request_options import RequestOptions +from httpx._types import RequestFiles + +INITIAL_RETRY_DELAY_SECONDS = 1.0 +MAX_RETRY_DELAY_SECONDS = 60.0 +JITTER_FACTOR = 0.2 # 20% random jitter + + +def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. + + Inspired by the urllib3 retry implementation. + """ + retry_after_ms = response_headers.get("retry-after-ms") + if retry_after_ms is not None: + try: + return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 + except Exception: + pass + + retry_after = response_headers.get("retry-after") + if retry_after is None: + return None + + # Attempt to parse the header as an int. + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = float(retry_after) + # Fallback to parsing it as a date. + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + return None + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + +def _add_positive_jitter(delay: float) -> float: + """Add positive jitter (0-20%) to prevent thundering herd.""" + jitter_multiplier = 1 + random() * JITTER_FACTOR + return delay * jitter_multiplier + + +def _add_symmetric_jitter(delay: float) -> float: + """Add symmetric jitter (±10%) for exponential backoff.""" + jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR + return delay * jitter_multiplier + + +def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + Parse the X-RateLimit-Reset header (Unix timestamp in seconds). + Returns seconds to wait, or None if header is missing/invalid. + """ + reset_time_str = response_headers.get("x-ratelimit-reset") + if reset_time_str is None: + return None + + try: + reset_time = int(reset_time_str) + delay = reset_time - time.time() + if delay > 0: + return delay + except (ValueError, TypeError): + pass + + return None + + +def _retry_timeout(response: httpx.Response, retries: int) -> float: + """ + Determine the amount of time to wait before retrying a request. + This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff + with a jitter to determine the number of seconds to wait. + """ + + # 1. Check Retry-After header first + retry_after = _parse_retry_after(response.headers) + if retry_after is not None and retry_after > 0: + return min(retry_after, MAX_RETRY_DELAY_SECONDS) + + # 2. Check X-RateLimit-Reset header (with positive jitter) + ratelimit_reset = _parse_x_ratelimit_reset(response.headers) + if ratelimit_reset is not None: + return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS)) + + # 3. Fall back to exponential backoff (with symmetric jitter) + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + return _add_symmetric_jitter(backoff) + + +def _retry_timeout_from_retries(retries: int) -> float: + """Determine retry timeout using exponential backoff when no response is available.""" + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + return _add_symmetric_jitter(backoff) + + +def _should_retry(response: httpx.Response) -> bool: + retryable_400s = [429, 408, 409] + return response.status_code >= 500 or response.status_code in retryable_400s + + +_SENSITIVE_HEADERS = frozenset( + { + "authorization", + "www-authenticate", + "x-api-key", + "api-key", + "apikey", + "x-api-token", + "x-auth-token", + "auth-token", + "cookie", + "set-cookie", + "proxy-authorization", + "proxy-authenticate", + "x-csrf-token", + "x-xsrf-token", + "x-session-token", + "x-access-token", + } +) + + +def _redact_headers(headers: typing.Dict[str, str]) -> typing.Dict[str, str]: + return {k: ("[REDACTED]" if k.lower() in _SENSITIVE_HEADERS else v) for k, v in headers.items()} + + +def _build_url(base_url: str, path: typing.Optional[str]) -> str: + """ + Build a full URL by joining a base URL with a path. + + This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs) + by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly + strip path components when the path starts with '/'. + + Example: + >>> _build_url("https://cloud.example.com/org/tenant/api", "/users") + 'https://cloud.example.com/org/tenant/api/users' + + Args: + base_url: The base URL, which may contain path prefixes. + path: The path to append. Can be None or empty string. + + Returns: + The full URL with base_url and path properly joined. + """ + if not path: + return base_url + return f"{base_url.rstrip('/')}/{path.lstrip('/')}" + + +def _maybe_filter_none_from_multipart_data( + data: typing.Optional[typing.Any], + request_files: typing.Optional[RequestFiles], + force_multipart: typing.Optional[bool], +) -> typing.Optional[typing.Any]: + """ + Filter None values from data body for multipart/form requests. + This prevents httpx from converting None to empty strings in multipart encoding. + Only applies when files are present or force_multipart is True. + """ + if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart): + return remove_none_from_dict(data) + return data + + +def remove_omit_from_dict( + original: typing.Dict[str, typing.Optional[typing.Any]], + omit: typing.Optional[typing.Any], +) -> typing.Dict[str, typing.Any]: + if omit is None: + return original + new: typing.Dict[str, typing.Any] = {} + for key, value in original.items(): + if value is not omit: + new[key] = value + return new + + +def maybe_filter_request_body( + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Optional[typing.Any]: + if data is None: + return ( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else None + ) + elif not isinstance(data, typing.Mapping): + data_content = jsonable_encoder(data) + else: + data_content = { + **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore + **( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else {} + ), + } + return data_content + + +# Abstracted out for testing purposes +def get_request_body( + *, + json: typing.Optional[typing.Any], + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]: + json_body = None + data_body = None + if data is not None: + data_body = maybe_filter_request_body(data, request_options, omit) + else: + # If both data and json are None, we send json data in the event extra properties are specified + json_body = maybe_filter_request_body(json, request_options, omit) + + has_additional_body_parameters = bool( + request_options is not None and request_options.get("additional_body_parameters") + ) + + # Only collapse empty dict to None when the body was not explicitly provided + # and there are no additional body parameters. This preserves explicit empty + # bodies (e.g., when an endpoint has a request body type but all fields are optional). + if json_body == {} and json is None and not has_additional_body_parameters: + json_body = None + if data_body == {} and data is None and not has_additional_body_parameters: + data_body = None + + return json_body, data_body + + +class HttpClient: + def __init__( + self, + *, + httpx_client: httpx.Client, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + base_max_retries: int = 2, + logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.base_max_retries = base_max_retries + self.httpx_client = httpx_client + self.logger = create_logger(logging_config) + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + has_body=json_body is not None or data_body is not None, + ) + + max_retries: int = ( + request_options.get("max_retries", self.base_max_retries) + if request_options is not None + else self.base_max_retries + ) + + try: + response = self.httpx_client.request( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + except (httpx.ConnectError, httpx.RemoteProtocolError): + if retries < max_retries: + time.sleep(_retry_timeout_from_retries(retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + raise + + if _should_retry(response=response): + if retries < max_retries: + time.sleep(_retry_timeout(response=response, retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + + if self.logger.is_debug(): + if 200 <= response.status_code < 400: + self.logger.debug( + "HTTP request succeeded", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + if self.logger.is_error(): + if response.status_code >= 400: + self.logger.error( + "HTTP request failed with error status", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + return response + + @contextmanager + def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.Iterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making streaming HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + ) + + with self.httpx_client.stream( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream + + +class AsyncHttpClient: + def __init__( + self, + *, + httpx_client: httpx.AsyncClient, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + base_max_retries: int = 2, + async_base_headers: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]]] = None, + logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.base_max_retries = base_max_retries + self.async_base_headers = async_base_headers + self.httpx_client = httpx_client + self.logger = create_logger(logging_config) + + async def _get_headers(self) -> typing.Dict[str, str]: + if self.async_base_headers is not None: + return await self.async_base_headers() + return self.base_headers() + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + async def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + has_body=json_body is not None or data_body is not None, + ) + + max_retries: int = ( + request_options.get("max_retries", self.base_max_retries) + if request_options is not None + else self.base_max_retries + ) + + try: + response = await self.httpx_client.request( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + except (httpx.ConnectError, httpx.RemoteProtocolError): + if retries < max_retries: + await asyncio.sleep(_retry_timeout_from_retries(retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + raise + + if _should_retry(response=response): + if retries < max_retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + + if self.logger.is_debug(): + if 200 <= response.status_code < 400: + self.logger.debug( + "HTTP request succeeded", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + if self.logger.is_error(): + if response.status_code >= 400: + self.logger.error( + "HTTP request failed with error status", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + return response + + @asynccontextmanager + async def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.AsyncIterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making streaming HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + ) + + async with self.httpx_client.stream( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_response.py b/seed/python-sdk/basic-auth-optional/src/seed/core/http_response.py new file mode 100644 index 000000000000..00bb1096d2d0 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/http_response.py @@ -0,0 +1,59 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Dict, Generic, TypeVar + +import httpx + +# Generic to represent the underlying type of the data wrapped by the HTTP response. +T = TypeVar("T") + + +class BaseHttpResponse: + """Minimalist HTTP response wrapper that exposes response headers and status code.""" + + _response: httpx.Response + + def __init__(self, response: httpx.Response): + self._response = response + + @property + def headers(self) -> Dict[str, str]: + return dict(self._response.headers) + + @property + def status_code(self) -> int: + return self._response.status_code + + +class HttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + def close(self) -> None: + self._response.close() + + +class AsyncHttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + async def close(self) -> None: + await self._response.aclose() diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/__init__.py b/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_api.py b/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_decoders.py b/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_exceptions.py b/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_models.py b/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/jsonable_encoder.py b/seed/python-sdk/basic-auth-optional/src/seed/core/jsonable_encoder.py new file mode 100644 index 000000000000..f8beaeafb17f --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/jsonable_encoder.py @@ -0,0 +1,108 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +jsonable_encoder converts a Python object to a JSON-friendly dict +(e.g. datetimes to strings, Pydantic models to dicts). + +Taken from FastAPI, and made a bit simpler +https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py +""" + +import base64 +import dataclasses +import datetime as dt +from enum import Enum +from pathlib import PurePath +from types import GeneratorType +from typing import Any, Callable, Dict, List, Optional, Set, Union + +import pydantic +from .datetime_utils import serialize_datetime +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + encode_by_type, + to_jsonable_with_fallback, +) + +SetIntStr = Set[Union[int, str]] +DictIntStrAny = Dict[Union[int, str], Any] + + +def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + custom_encoder = custom_encoder or {} + # Generated SDKs use Ellipsis (`...`) as the sentinel value for "OMIT". + # OMIT values should be excluded from serialized payloads. + if obj is Ellipsis: + return None + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + if isinstance(obj, pydantic.BaseModel): + if IS_PYDANTIC_V2: + encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 + else: + encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 + if custom_encoder: + encoder.update(custom_encoder) + obj_dict = obj.dict(by_alias=True) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + if "root" in obj_dict: + obj_dict = obj_dict["root"] + return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) # type: ignore + return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dict): + encoded_dict = {} + allowed_keys = set(obj.keys()) + for key, value in obj.items(): + if key in allowed_keys: + if value is Ellipsis: + continue + encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) + encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + encoded_list = [] + for item in obj: + if item is Ellipsis: + continue + encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) + return encoded_list + + def fallback_serializer(o: Any) -> Any: + attempt_encode = encode_by_type(o) + if attempt_encode is not None: + return attempt_encode + + try: + data = dict(o) + except Exception as e: + errors: List[Exception] = [] + errors.append(e) + try: + data = vars(o) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) + + return to_jsonable_with_fallback(obj, fallback_serializer) diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/logging.py b/seed/python-sdk/basic-auth-optional/src/seed/core/logging.py new file mode 100644 index 000000000000..e5e572458bc8 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/logging.py @@ -0,0 +1,107 @@ +# This file was auto-generated by Fern from our API Definition. + +import logging +import typing + +LogLevel = typing.Literal["debug", "info", "warn", "error"] + +_LOG_LEVEL_MAP: typing.Dict[LogLevel, int] = { + "debug": 1, + "info": 2, + "warn": 3, + "error": 4, +} + + +class ILogger(typing.Protocol): + def debug(self, message: str, **kwargs: typing.Any) -> None: ... + def info(self, message: str, **kwargs: typing.Any) -> None: ... + def warn(self, message: str, **kwargs: typing.Any) -> None: ... + def error(self, message: str, **kwargs: typing.Any) -> None: ... + + +class ConsoleLogger: + _logger: logging.Logger + + def __init__(self) -> None: + self._logger = logging.getLogger("fern") + if not self._logger.handlers: + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(levelname)s - %(message)s")) + self._logger.addHandler(handler) + self._logger.setLevel(logging.DEBUG) + + def debug(self, message: str, **kwargs: typing.Any) -> None: + self._logger.debug(message, extra=kwargs) + + def info(self, message: str, **kwargs: typing.Any) -> None: + self._logger.info(message, extra=kwargs) + + def warn(self, message: str, **kwargs: typing.Any) -> None: + self._logger.warning(message, extra=kwargs) + + def error(self, message: str, **kwargs: typing.Any) -> None: + self._logger.error(message, extra=kwargs) + + +class LogConfig(typing.TypedDict, total=False): + level: LogLevel + logger: ILogger + silent: bool + + +class Logger: + _level: int + _logger: ILogger + _silent: bool + + def __init__(self, *, level: LogLevel, logger: ILogger, silent: bool) -> None: + self._level = _LOG_LEVEL_MAP[level] + self._logger = logger + self._silent = silent + + def _should_log(self, level: LogLevel) -> bool: + return not self._silent and self._level <= _LOG_LEVEL_MAP[level] + + def is_debug(self) -> bool: + return self._should_log("debug") + + def is_info(self) -> bool: + return self._should_log("info") + + def is_warn(self) -> bool: + return self._should_log("warn") + + def is_error(self) -> bool: + return self._should_log("error") + + def debug(self, message: str, **kwargs: typing.Any) -> None: + if self.is_debug(): + self._logger.debug(message, **kwargs) + + def info(self, message: str, **kwargs: typing.Any) -> None: + if self.is_info(): + self._logger.info(message, **kwargs) + + def warn(self, message: str, **kwargs: typing.Any) -> None: + if self.is_warn(): + self._logger.warn(message, **kwargs) + + def error(self, message: str, **kwargs: typing.Any) -> None: + if self.is_error(): + self._logger.error(message, **kwargs) + + +_default_logger: Logger = Logger(level="info", logger=ConsoleLogger(), silent=True) + + +def create_logger(config: typing.Optional[typing.Union[LogConfig, Logger]] = None) -> Logger: + if config is None: + return _default_logger + if isinstance(config, Logger): + return config + return Logger( + level=config.get("level", "info"), + logger=config.get("logger", ConsoleLogger()), + silent=config.get("silent", True), + ) diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/parse_error.py b/seed/python-sdk/basic-auth-optional/src/seed/core/parse_error.py new file mode 100644 index 000000000000..4527c6a8adec --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/parse_error.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ParsingError(Exception): + """ + Raised when the SDK fails to parse/validate a response from the server. + This typically indicates that the server returned a response whose shape + does not match the expected schema. + """ + + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + cause: Optional[Exception] + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + cause: Optional[Exception] = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + self.cause = cause + super().__init__() + if cause is not None: + self.__cause__ = cause + + def __str__(self) -> str: + cause_str = f", cause: {self.cause}" if self.cause is not None else "" + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}{cause_str}" diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/pydantic_utilities.py b/seed/python-sdk/basic-auth-optional/src/seed/core/pydantic_utilities.py new file mode 100644 index 000000000000..fea3a08d3268 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/pydantic_utilities.py @@ -0,0 +1,634 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +import inspect +import json +import logging +from collections import defaultdict +from dataclasses import asdict +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Dict, + List, + Mapping, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +import pydantic +import typing_extensions +from pydantic.fields import FieldInfo as _FieldInfo + +_logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from .http_sse._models import ServerSentEvent + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + _datetime_adapter = pydantic.TypeAdapter(dt.datetime) # type: ignore[attr-defined] + _date_adapter = pydantic.TypeAdapter(dt.date) # type: ignore[attr-defined] + + def parse_datetime(value: Any) -> dt.datetime: # type: ignore[misc] + if isinstance(value, dt.datetime): + return value + return _datetime_adapter.validate_python(value) + + def parse_date(value: Any) -> dt.date: # type: ignore[misc] + if isinstance(value, dt.datetime): + return value.date() + if isinstance(value, dt.date): + return value + return _date_adapter.validate_python(value) + + # Avoid importing from pydantic.v1 to maintain Python 3.14 compatibility. + from typing import get_args as get_args # type: ignore[assignment] + from typing import get_origin as get_origin # type: ignore[assignment] + + def is_literal_type(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] + return typing_extensions.get_origin(tp) is typing_extensions.Literal + + def is_union(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] + return tp is Union or typing_extensions.get_origin(tp) is Union # type: ignore[comparison-overlap] + + # Inline encoders_by_type to avoid importing from pydantic.v1.json + import re as _re + from collections import deque as _deque + from decimal import Decimal as _Decimal + from enum import Enum as _Enum + from ipaddress import ( + IPv4Address as _IPv4Address, + ) + from ipaddress import ( + IPv4Interface as _IPv4Interface, + ) + from ipaddress import ( + IPv4Network as _IPv4Network, + ) + from ipaddress import ( + IPv6Address as _IPv6Address, + ) + from ipaddress import ( + IPv6Interface as _IPv6Interface, + ) + from ipaddress import ( + IPv6Network as _IPv6Network, + ) + from pathlib import Path as _Path + from types import GeneratorType as _GeneratorType + from uuid import UUID as _UUID + + from pydantic.fields import FieldInfo as ModelField # type: ignore[no-redef, assignment] + + def _decimal_encoder(dec_value: Any) -> Any: + if dec_value.as_tuple().exponent >= 0: + return int(dec_value) + return float(dec_value) + + encoders_by_type: Dict[Type[Any], Callable[[Any], Any]] = { # type: ignore[no-redef] + bytes: lambda o: o.decode(), + dt.date: lambda o: o.isoformat(), + dt.datetime: lambda o: o.isoformat(), + dt.time: lambda o: o.isoformat(), + dt.timedelta: lambda td: td.total_seconds(), + _Decimal: _decimal_encoder, + _Enum: lambda o: o.value, + frozenset: list, + _deque: list, + _GeneratorType: list, + _IPv4Address: str, + _IPv4Interface: str, + _IPv4Network: str, + _IPv6Address: str, + _IPv6Interface: str, + _IPv6Network: str, + _Path: str, + _re.Pattern: lambda o: o.pattern, + set: list, + _UUID: str, + } +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef] + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef] + from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef, assignment] + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[no-redef] + from pydantic.typing import get_args as get_args # type: ignore[no-redef] + from pydantic.typing import get_origin as get_origin # type: ignore[no-redef] + from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef, assignment] + from pydantic.typing import is_union as is_union # type: ignore[no-redef] + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata +from typing_extensions import TypeAlias + +T = TypeVar("T") +Model = TypeVar("Model", bound=pydantic.BaseModel) + + +def _get_discriminator_and_variants(type_: Type[Any]) -> Tuple[Optional[str], Optional[List[Type[Any]]]]: + """ + Extract the discriminator field name and union variants from a discriminated union type. + Supports Annotated[Union[...], Field(discriminator=...)] patterns. + Returns (discriminator, variants) or (None, None) if not a discriminated union. + """ + origin = typing_extensions.get_origin(type_) + + if origin is typing_extensions.Annotated: + args = typing_extensions.get_args(type_) + if len(args) >= 2: + inner_type = args[0] + # Check annotations for discriminator + discriminator = None + for annotation in args[1:]: + if hasattr(annotation, "discriminator"): + discriminator = getattr(annotation, "discriminator", None) + break + + if discriminator: + inner_origin = typing_extensions.get_origin(inner_type) + if inner_origin is Union: + variants = list(typing_extensions.get_args(inner_type)) + return discriminator, variants + return None, None + + +def _get_field_annotation(model: Type[Any], field_name: str) -> Optional[Type[Any]]: + """Get the type annotation of a field from a Pydantic model.""" + if IS_PYDANTIC_V2: + fields = getattr(model, "model_fields", {}) + field_info = fields.get(field_name) + if field_info: + return cast(Optional[Type[Any]], field_info.annotation) + else: + fields = getattr(model, "__fields__", {}) + field_info = fields.get(field_name) + if field_info: + return cast(Optional[Type[Any]], field_info.outer_type_) + return None + + +def _find_variant_by_discriminator( + variants: List[Type[Any]], + discriminator: str, + discriminator_value: Any, +) -> Optional[Type[Any]]: + """Find the union variant that matches the discriminator value.""" + for variant in variants: + if not (inspect.isclass(variant) and issubclass(variant, pydantic.BaseModel)): + continue + + disc_annotation = _get_field_annotation(variant, discriminator) + if disc_annotation and is_literal_type(disc_annotation): + literal_args = get_args(disc_annotation) + if literal_args and literal_args[0] == discriminator_value: + return variant + return None + + +def _is_string_type(type_: Type[Any]) -> bool: + """Check if a type is str or Optional[str].""" + if type_ is str: + return True + + origin = typing_extensions.get_origin(type_) + if origin is Union: + args = typing_extensions.get_args(type_) + # Optional[str] = Union[str, None] + non_none_args = [a for a in args if a is not type(None)] + if len(non_none_args) == 1 and non_none_args[0] is str: + return True + + return False + + +def parse_sse_obj(sse: "ServerSentEvent", type_: Type[T]) -> T: + """ + Parse a ServerSentEvent into the appropriate type. + + Handles two scenarios based on where the discriminator field is located: + + 1. Data-level discrimination: The discriminator (e.g., 'type') is inside the 'data' payload. + The union describes the data content, not the SSE envelope. + -> Returns: json.loads(data) parsed into the type + + Example: ChatStreamResponse with discriminator='type' + Input: ServerSentEvent(event="message", data='{"type": "content-delta", ...}', id="") + Output: ContentDeltaEvent (parsed from data, SSE envelope stripped) + + 2. Event-level discrimination: The discriminator (e.g., 'event') is at the SSE event level. + The union describes the full SSE event structure. + -> Returns: SSE envelope with 'data' field JSON-parsed only if the variant expects non-string + + Example: JobStreamResponse with discriminator='event' + Input: ServerSentEvent(event="ERROR", data='{"code": "FAILED", ...}', id="123") + Output: JobStreamResponse_Error with data as ErrorData object + + But for variants where data is str (like STATUS_UPDATE): + Input: ServerSentEvent(event="STATUS_UPDATE", data='{"status": "processing"}', id="1") + Output: JobStreamResponse_StatusUpdate with data as string (not parsed) + + Args: + sse: The ServerSentEvent object to parse + type_: The target discriminated union type + + Returns: + The parsed object of type T + + Note: + This function is only available in SDK contexts where http_sse module exists. + """ + sse_event = asdict(sse) + discriminator, variants = _get_discriminator_and_variants(type_) + + if discriminator is None or variants is None: + # Not a discriminated union - parse the data field as JSON + data_value = sse_event.get("data") + if isinstance(data_value, str) and data_value: + try: + parsed_data = json.loads(data_value) + return parse_obj_as(type_, parsed_data) + except json.JSONDecodeError as e: + _logger.warning( + "Failed to parse SSE data field as JSON: %s, data: %s", + e, + data_value[:100] if len(data_value) > 100 else data_value, + ) + return parse_obj_as(type_, sse_event) + + data_value = sse_event.get("data") + + # Check if discriminator is at the top level (event-level discrimination) + if discriminator in sse_event: + # Case 2: Event-level discrimination + # Find the matching variant to check if 'data' field needs JSON parsing + disc_value = sse_event.get(discriminator) + matching_variant = _find_variant_by_discriminator(variants, discriminator, disc_value) + + if matching_variant is not None: + # Check what type the variant expects for 'data' + data_type = _get_field_annotation(matching_variant, "data") + if data_type is not None and not _is_string_type(data_type): + # Variant expects non-string data - parse JSON + if isinstance(data_value, str) and data_value: + try: + parsed_data = json.loads(data_value) + new_object = dict(sse_event) + new_object["data"] = parsed_data + return parse_obj_as(type_, new_object) + except json.JSONDecodeError as e: + _logger.warning( + "Failed to parse SSE data field as JSON for event-level discrimination: %s, data: %s", + e, + data_value[:100] if len(data_value) > 100 else data_value, + ) + # Either no matching variant, data is string type, or JSON parse failed + return parse_obj_as(type_, sse_event) + + else: + # Case 1: Data-level discrimination + # The discriminator is inside the data payload - extract and parse data only + if isinstance(data_value, str) and data_value: + try: + parsed_data = json.loads(data_value) + return parse_obj_as(type_, parsed_data) + except json.JSONDecodeError as e: + _logger.warning( + "Failed to parse SSE data field as JSON for data-level discrimination: %s, data: %s", + e, + data_value[:100] if len(data_value) > 100 else data_value, + ) + return parse_obj_as(type_, sse_event) + + +def parse_obj_as(type_: Type[T], object_: Any) -> T: + # convert_and_respect_annotation_metadata is required for TypedDict aliasing. + # + # For Pydantic models, whether we should pre-dealias depends on how the model encodes aliasing: + # - If the model uses real Pydantic aliases (pydantic.Field(alias=...)), then we must pass wire keys through + # unchanged so Pydantic can validate them. + # - If the model encodes aliasing only via FieldMetadata annotations, then we MUST pre-dealias because Pydantic + # will not recognize those aliases during validation. + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + has_pydantic_aliases = False + if IS_PYDANTIC_V2: + for field_name, field_info in getattr(type_, "model_fields", {}).items(): # type: ignore[attr-defined] + alias = getattr(field_info, "alias", None) + if alias is not None and alias != field_name: + has_pydantic_aliases = True + break + else: + for field in getattr(type_, "__fields__", {}).values(): + alias = getattr(field, "alias", None) + name = getattr(field, "name", None) + if alias is not None and name is not None and alias != name: + has_pydantic_aliases = True + break + + dealiased_object = ( + object_ + if has_pydantic_aliases + else convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + ) + else: + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + if IS_PYDANTIC_V2: + adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + return adapter.validate_python(dealiased_object) + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any]) -> Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( # type: ignore[typeddict-unknown-key] + # Allow fields beginning with `model_` to be used in the model + protected_namespaces=(), + ) + + @pydantic.model_validator(mode="before") # type: ignore[attr-defined] + @classmethod + def _coerce_field_names_to_aliases(cls, data: Any) -> Any: + """ + Accept Python field names in input by rewriting them to their Pydantic aliases, + while avoiding silent collisions when a key could refer to multiple fields. + """ + if not isinstance(data, Mapping): + return data + + fields = getattr(cls, "model_fields", {}) # type: ignore[attr-defined] + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field_info in fields.items(): + alias = getattr(field_info, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + # Detect ambiguous keys: a key that is an alias for one field and a name for another. + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in data and name_to_alias[key] not in data: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(data.keys()) + rewritten: Dict[str, Any] = dict(data) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + + @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] + def serialize_model(self) -> Any: # type: ignore[name-defined] + serialized = self.dict() # type: ignore[attr-defined] + data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()} + return data + + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + @pydantic.root_validator(pre=True) + def _coerce_field_names_to_aliases(cls, values: Any) -> Any: + """ + Pydantic v1 equivalent of _coerce_field_names_to_aliases. + """ + if not isinstance(values, Mapping): + return values + + fields = getattr(cls, "__fields__", {}) + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field in fields.items(): + alias = getattr(field, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in values and name_to_alias[key] not in values: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(values.keys()) + rewritten: Dict[str, Any] = dict(values) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + + @classmethod + def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + return cls.construct(_fields_set, **dealiased_object) + + @classmethod + def construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + if IS_PYDANTIC_V2: + return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc] + return super().construct(_fields_set, **dealiased_object) + + def json(self, **kwargs: Any) -> str: + kwargs_with_defaults = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc] + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: Any) -> Dict[str, Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multiplexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore[misc] + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc] + ) + + else: + _fields_set = self.__fields_set__.copy() + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default is not None or ("exclude_unset" in kwargs and not kwargs["exclude_unset"]): + _fields_set.add(name) + + if default is not None: + self.__fields_set__.add(name) + + kwargs_with_defaults_exclude_unset_include_fields = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) + + return cast( + Dict[str, Any], + convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write"), + ) + + +def _union_list_of_pydantic_dicts(source: List[Any], destination: List[Any]) -> List[Any]: + converted_list: List[Any] = [] + for i, item in enumerate(source): + destination_value = destination[i] + if isinstance(item, dict): + converted_list.append(deep_union_pydantic_dicts(item, destination_value)) + elif isinstance(item, list): + converted_list.append(_union_list_of_pydantic_dicts(item, destination_value)) + else: + converted_list.append(item) + return converted_list + + +def deep_union_pydantic_dicts(source: Dict[str, Any], destination: Dict[str, Any]) -> Dict[str, Any]: + for key, value in source.items(): + node = destination.setdefault(key, {}) + if isinstance(value, dict): + deep_union_pydantic_dicts(value, node) + # Note: we do not do this same processing for sets given we do not have sets of models + # and given the sets are unordered, the processing of the set and matching objects would + # be non-trivial. + elif isinstance(value, list): + destination[key] = _union_list_of_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc, name-defined, type-arg] + pass + + UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc] +else: + UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef] + + +def encode_by_type(o: Any) -> Any: + encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: Type["Model"], **localns: Any) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore[attr-defined] + else: + model.update_forward_refs(**localns) + + +# Mirrors Pydantic's internal typing +AnyCallable = Callable[..., Any] + + +def universal_root_validator( + pre: bool = False, +) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + # In Pydantic v2, for RootModel we always use "before" mode + # The custom validators transform the input value before the model is created + return cast(AnyCallable, pydantic.model_validator(mode="before")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] + + return decorator + + +def universal_field_validator(field_name: str, pre: bool = False) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.field_validator(field_name, mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) + + return decorator + + +PydanticField = Union[ModelField, _FieldInfo] + + +def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined] + return cast(Mapping[str, PydanticField], model.__fields__) + + +def _get_field_default(field: PydanticField) -> Any: + try: + value = field.get_default() # type: ignore[union-attr] + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/query_encoder.py b/seed/python-sdk/basic-auth-optional/src/seed/core/query_encoder.py new file mode 100644 index 000000000000..3183001d4046 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/query_encoder.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, List, Optional, Tuple + +import pydantic + + +# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict +def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> List[Tuple[str, Any]]: + result = [] + for k, v in dict_flat.items(): + key = f"{key_prefix}[{k}]" if key_prefix is not None else k + if isinstance(v, dict): + result.extend(traverse_query_dict(v, key)) + elif isinstance(v, list): + for arr_v in v: + if isinstance(arr_v, dict): + result.extend(traverse_query_dict(arr_v, key)) + else: + result.append((key, arr_v)) + else: + result.append((key, v)) + return result + + +def single_query_encoder(query_key: str, query_value: Any) -> List[Tuple[str, Any]]: + if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic.BaseModel): + obj_dict = query_value.dict(by_alias=True) + else: + obj_dict = query_value + return traverse_query_dict(obj_dict, query_key) + elif isinstance(query_value, list): + encoded_values: List[Tuple[str, Any]] = [] + for value in query_value: + if isinstance(value, pydantic.BaseModel) or isinstance(value, dict): + if isinstance(value, pydantic.BaseModel): + obj_dict = value.dict(by_alias=True) + elif isinstance(value, dict): + obj_dict = value + + encoded_values.extend(single_query_encoder(query_key, obj_dict)) + else: + encoded_values.append((query_key, value)) + + return encoded_values + + return [(query_key, query_value)] + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[List[Tuple[str, Any]]]: + if query is None: + return None + + encoded_query = [] + for k, v in query.items(): + encoded_query.extend(single_query_encoder(k, v)) + return encoded_query diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/remove_none_from_dict.py b/seed/python-sdk/basic-auth-optional/src/seed/core/remove_none_from_dict.py new file mode 100644 index 000000000000..c2298143f14a --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Mapping, Optional + + +def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/request_options.py b/seed/python-sdk/basic-auth-optional/src/seed/core/request_options.py new file mode 100644 index 000000000000..1b38804432ba --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/request_options.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +try: + from typing import NotRequired # type: ignore +except ImportError: + from typing_extensions import NotRequired + + +class RequestOptions(typing.TypedDict, total=False): + """ + Additional options for request-specific configuration when calling APIs via the SDK. + This is used primarily as an optional final parameter for service functions. + + Attributes: + - timeout_in_seconds: int. The number of seconds to await an API call before timing out. + + - max_retries: int. The max number of retries to attempt if the API call fails. + + - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict + + - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict + + - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + + - chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads. + """ + + timeout_in_seconds: NotRequired[int] + max_retries: NotRequired[int] + additional_headers: NotRequired[typing.Dict[str, typing.Any]] + additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] + additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] + chunk_size: NotRequired[int] diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/serialization.py b/seed/python-sdk/basic-auth-optional/src/seed/core/serialization.py new file mode 100644 index 000000000000..c36e865cc729 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/serialization.py @@ -0,0 +1,276 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import inspect +import typing + +import pydantic +import typing_extensions + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): + return _convert_mapping(object_, clean_type, direction) + + if ( + typing_extensions.get_origin(clean_type) == typing.Dict + or typing_extensions.get_origin(clean_type) == dict + or clean_type == typing.Dict + ) and isinstance(object_, typing.Dict): + key_type = typing_extensions.get_args(clean_type)[0] + value_type = typing_extensions.get_args(clean_type)[1] + + return { + key: convert_and_respect_annotation_metadata( + object_=value, + annotation=annotation, + inner_type=value_type, + direction=direction, + ) + for key, value in object_.items() + } + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + try: + annotations = typing_extensions.get_type_hints(expected_type, include_extras=True) + except NameError: + # The TypedDict contains a circular reference, so + # we use the __annotations__ attribute directly. + annotations = getattr(expected_type, "__annotations__", {}) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[_alias_key(key, type_, direction, aliases_to_field_names)] = ( + convert_and_respect_annotation_metadata(object_=value, annotation=type_, direction=direction) + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/__init__.py b/seed/python-sdk/basic-auth-optional/src/seed/errors/__init__.py new file mode 100644 index 000000000000..cd9c162d3e99 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/errors/__init__.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import UnauthorizedRequestErrorBody + from .errors import BadRequest, UnauthorizedRequest +_dynamic_imports: typing.Dict[str, str] = { + "BadRequest": ".errors", + "UnauthorizedRequest": ".errors", + "UnauthorizedRequestErrorBody": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["BadRequest", "UnauthorizedRequest", "UnauthorizedRequestErrorBody"] diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/__init__.py b/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/__init__.py new file mode 100644 index 000000000000..786cc9a53f3b --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/__init__.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .bad_request import BadRequest + from .unauthorized_request import UnauthorizedRequest +_dynamic_imports: typing.Dict[str, str] = {"BadRequest": ".bad_request", "UnauthorizedRequest": ".unauthorized_request"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["BadRequest", "UnauthorizedRequest"] diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/bad_request.py b/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/bad_request.py new file mode 100644 index 000000000000..634bebc60527 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/bad_request.py @@ -0,0 +1,13 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class BadRequest(ApiError): + def __init__(self, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__( + status_code=400, + headers=headers, + ) diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/unauthorized_request.py b/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/unauthorized_request.py new file mode 100644 index 000000000000..1ac2826faad6 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/unauthorized_request.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError +from ..types.unauthorized_request_error_body import UnauthorizedRequestErrorBody + + +class UnauthorizedRequest(ApiError): + def __init__(self, body: UnauthorizedRequestErrorBody, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=401, headers=headers, body=body) diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/types/__init__.py b/seed/python-sdk/basic-auth-optional/src/seed/errors/types/__init__.py new file mode 100644 index 000000000000..dad37fbdca08 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/errors/types/__init__.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .unauthorized_request_error_body import UnauthorizedRequestErrorBody +_dynamic_imports: typing.Dict[str, str] = {"UnauthorizedRequestErrorBody": ".unauthorized_request_error_body"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["UnauthorizedRequestErrorBody"] diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/types/unauthorized_request_error_body.py b/seed/python-sdk/basic-auth-optional/src/seed/errors/types/unauthorized_request_error_body.py new file mode 100644 index 000000000000..dea32e5165e8 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/errors/types/unauthorized_request_error_body.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class UnauthorizedRequestErrorBody(UniversalBaseModel): + message: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/basic-auth-optional/src/seed/py.typed b/seed/python-sdk/basic-auth-optional/src/seed/py.typed new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/seed/python-sdk/basic-auth-optional/src/seed/version.py b/seed/python-sdk/basic-auth-optional/src/seed/version.py new file mode 100644 index 000000000000..b8bd9635d24c --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/src/seed/version.py @@ -0,0 +1,3 @@ +from importlib import metadata + +__version__ = metadata.version("fern_basic-auth-optional") diff --git a/seed/python-sdk/basic-auth-optional/tests/custom/test_client.py b/seed/python-sdk/basic-auth-optional/tests/custom/test_client.py new file mode 100644 index 000000000000..ab04ce6393ef --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/custom/test_client.py @@ -0,0 +1,7 @@ +import pytest + + +# Get started with writing tests with pytest at https://docs.pytest.org +@pytest.mark.skip(reason="Unimplemented") +def test_client() -> None: + assert True diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/__init__.py b/seed/python-sdk/basic-auth-optional/tests/utils/__init__.py new file mode 100644 index 000000000000..f3ea2659bb1c --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/__init__.py @@ -0,0 +1,2 @@ +# This file was auto-generated by Fern from our API Definition. + diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/__init__.py b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/__init__.py new file mode 100644 index 000000000000..2cf01263529d --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/__init__.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from .circle import CircleParams +from .object_with_defaults import ObjectWithDefaultsParams +from .object_with_optional_field import ObjectWithOptionalFieldParams +from .shape import Shape_CircleParams, Shape_SquareParams, ShapeParams +from .square import SquareParams +from .undiscriminated_shape import UndiscriminatedShapeParams + +__all__ = [ + "CircleParams", + "ObjectWithDefaultsParams", + "ObjectWithOptionalFieldParams", + "ShapeParams", + "Shape_CircleParams", + "Shape_SquareParams", + "SquareParams", + "UndiscriminatedShapeParams", +] diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/circle.py b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/circle.py new file mode 100644 index 000000000000..74ecf38c308b --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/circle.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + +from seed.core.serialization import FieldMetadata + + +class CircleParams(typing_extensions.TypedDict): + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/color.py b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/color.py new file mode 100644 index 000000000000..2aa2c4c52f0c --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/color.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing + +Color = typing.Union[typing.Literal["red", "blue"], typing.Any] diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_defaults.py b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_defaults.py new file mode 100644 index 000000000000..a977b1d2aa1c --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_defaults.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ObjectWithDefaultsParams(typing_extensions.TypedDict): + """ + Defines properties with default values and validation rules. + """ + + decimal: typing_extensions.NotRequired[float] + string: typing_extensions.NotRequired[str] + required_string: str diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_optional_field.py b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_optional_field.py new file mode 100644 index 000000000000..6b5608bc05b6 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_optional_field.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +import uuid + +import typing_extensions +from .color import Color +from .shape import ShapeParams +from .undiscriminated_shape import UndiscriminatedShapeParams + +from seed.core.serialization import FieldMetadata + + +class ObjectWithOptionalFieldParams(typing_extensions.TypedDict): + literal: typing.Literal["lit_one"] + string: typing_extensions.NotRequired[str] + integer: typing_extensions.NotRequired[int] + long_: typing_extensions.NotRequired[typing_extensions.Annotated[int, FieldMetadata(alias="long")]] + double: typing_extensions.NotRequired[float] + bool_: typing_extensions.NotRequired[typing_extensions.Annotated[bool, FieldMetadata(alias="bool")]] + datetime: typing_extensions.NotRequired[dt.datetime] + date: typing_extensions.NotRequired[dt.date] + uuid_: typing_extensions.NotRequired[typing_extensions.Annotated[uuid.UUID, FieldMetadata(alias="uuid")]] + base_64: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="base64")]] + list_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Sequence[str], FieldMetadata(alias="list")]] + set_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Set[str], FieldMetadata(alias="set")]] + map_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Dict[int, str], FieldMetadata(alias="map")]] + enum: typing_extensions.NotRequired[Color] + union: typing_extensions.NotRequired[ShapeParams] + second_union: typing_extensions.NotRequired[ShapeParams] + undiscriminated_union: typing_extensions.NotRequired[UndiscriminatedShapeParams] + any: typing.Optional[typing.Any] diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/shape.py b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/shape.py new file mode 100644 index 000000000000..7e70010a251f --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/shape.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import typing_extensions + +from seed.core.serialization import FieldMetadata + + +class Base(typing_extensions.TypedDict): + id: str + + +class Shape_CircleParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["circle"], FieldMetadata(alias="shapeType")] + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] + + +class Shape_SquareParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["square"], FieldMetadata(alias="shapeType")] + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] + + +ShapeParams = typing.Union[Shape_CircleParams, Shape_SquareParams] diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/square.py b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/square.py new file mode 100644 index 000000000000..71c7d25fd4ad --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/square.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + +from seed.core.serialization import FieldMetadata + + +class SquareParams(typing_extensions.TypedDict): + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/undiscriminated_shape.py b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/undiscriminated_shape.py new file mode 100644 index 000000000000..99f12b300d1d --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/undiscriminated_shape.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .circle import CircleParams +from .square import SquareParams + +UndiscriminatedShapeParams = typing.Union[CircleParams, SquareParams] diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/test_http_client.py b/seed/python-sdk/basic-auth-optional/tests/utils/test_http_client.py new file mode 100644 index 000000000000..aa2a8b4e4700 --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/test_http_client.py @@ -0,0 +1,662 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict +from unittest.mock import AsyncMock, MagicMock, patch + +import httpx +import pytest + +from seed.core.http_client import ( + AsyncHttpClient, + HttpClient, + _build_url, + get_request_body, + remove_none_from_dict, +) +from seed.core.request_options import RequestOptions + + +# Stub clients for testing HttpClient and AsyncHttpClient +class _DummySyncClient: + """A minimal stub for httpx.Client that records request arguments.""" + + def __init__(self) -> None: + self.last_request_kwargs: Dict[str, Any] = {} + + def request(self, **kwargs: Any) -> "_DummyResponse": + self.last_request_kwargs = kwargs + return _DummyResponse() + + +class _DummyAsyncClient: + """A minimal stub for httpx.AsyncClient that records request arguments.""" + + def __init__(self) -> None: + self.last_request_kwargs: Dict[str, Any] = {} + + async def request(self, **kwargs: Any) -> "_DummyResponse": + self.last_request_kwargs = kwargs + return _DummyResponse() + + +class _DummyResponse: + """A minimal stub for httpx.Response.""" + + status_code = 200 + headers: Dict[str, str] = {} + + +def get_request_options() -> RequestOptions: + return {"additional_body_parameters": {"see you": "later"}} + + +def get_request_options_with_none() -> RequestOptions: + return {"additional_body_parameters": {"see you": "later", "optional": None}} + + +def test_get_json_request_body() -> None: + json_body, data_body = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=None) + assert json_body == {"hello": "world"} + assert data_body is None + + json_body_extras, data_body_extras = get_request_body( + json={"goodbye": "world"}, data=None, request_options=get_request_options(), omit=None + ) + + assert json_body_extras == {"goodbye": "world", "see you": "later"} + assert data_body_extras is None + + +def test_get_files_request_body() -> None: + json_body, data_body = get_request_body(json=None, data={"hello": "world"}, request_options=None, omit=None) + assert data_body == {"hello": "world"} + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data={"goodbye": "world"}, request_options=get_request_options(), omit=None + ) + + assert data_body_extras == {"goodbye": "world", "see you": "later"} + assert json_body_extras is None + + +def test_get_none_request_body() -> None: + json_body, data_body = get_request_body(json=None, data=None, request_options=None, omit=None) + assert data_body is None + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data=None, request_options=get_request_options(), omit=None + ) + + assert json_body_extras == {"see you": "later"} + assert data_body_extras is None + + +def test_get_empty_json_request_body() -> None: + """Test that implicit empty bodies (json=None) are collapsed to None.""" + unrelated_request_options: RequestOptions = {"max_retries": 3} + json_body, data_body = get_request_body(json=None, data=None, request_options=unrelated_request_options, omit=None) + assert json_body is None + assert data_body is None + + +def test_explicit_empty_json_body_is_preserved() -> None: + """Test that explicit empty bodies (json={}) are preserved and sent as {}. + + This is important for endpoints where the request body is required but all + fields are optional. The server expects valid JSON ({}) not an empty body. + """ + unrelated_request_options: RequestOptions = {"max_retries": 3} + + # Explicit json={} should be preserved + json_body, data_body = get_request_body(json={}, data=None, request_options=unrelated_request_options, omit=None) + assert json_body == {} + assert data_body is None + + # Explicit data={} should also be preserved + json_body2, data_body2 = get_request_body(json=None, data={}, request_options=unrelated_request_options, omit=None) + assert json_body2 is None + assert data_body2 == {} + + +def test_json_body_preserves_none_values() -> None: + """Test that JSON bodies preserve None values (they become JSON null).""" + json_body, data_body = get_request_body( + json={"hello": "world", "optional": None}, data=None, request_options=None, omit=None + ) + # JSON bodies should preserve None values + assert json_body == {"hello": "world", "optional": None} + assert data_body is None + + +def test_data_body_preserves_none_values_without_multipart() -> None: + """Test that data bodies preserve None values when not using multipart. + + The filtering of None values happens in HttpClient.request/stream methods, + not in get_request_body. This test verifies get_request_body doesn't filter None. + """ + json_body, data_body = get_request_body( + json=None, data={"hello": "world", "optional": None}, request_options=None, omit=None + ) + # get_request_body should preserve None values in data body + # The filtering happens later in HttpClient.request when multipart is detected + assert data_body == {"hello": "world", "optional": None} + assert json_body is None + + +def test_remove_none_from_dict_filters_none_values() -> None: + """Test that remove_none_from_dict correctly filters out None values.""" + original = {"hello": "world", "optional": None, "another": "value", "also_none": None} + filtered = remove_none_from_dict(original) + assert filtered == {"hello": "world", "another": "value"} + # Original should not be modified + assert original == {"hello": "world", "optional": None, "another": "value", "also_none": None} + + +def test_remove_none_from_dict_empty_dict() -> None: + """Test that remove_none_from_dict handles empty dict.""" + assert remove_none_from_dict({}) == {} + + +def test_remove_none_from_dict_all_none() -> None: + """Test that remove_none_from_dict handles dict with all None values.""" + assert remove_none_from_dict({"a": None, "b": None}) == {} + + +def test_http_client_does_not_pass_empty_params_list() -> None: + """Test that HttpClient passes params=None when params are empty. + + This prevents httpx from stripping existing query parameters from the URL, + which happens when params=[] or params={} is passed. + """ + dummy_client = _DummySyncClient() + http_client = HttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + ) + + # Use a path with query params (e.g., pagination cursor URL) + http_client.request( + path="resource?after=123", + method="GET", + params=None, + request_options=None, + ) + + # We care that httpx receives params=None, not [] or {} + assert "params" in dummy_client.last_request_kwargs + assert dummy_client.last_request_kwargs["params"] is None + + # Verify the query string in the URL is preserved + url = str(dummy_client.last_request_kwargs["url"]) + assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}" + + +def test_http_client_passes_encoded_params_when_present() -> None: + """Test that HttpClient passes encoded params when params are provided.""" + dummy_client = _DummySyncClient() + http_client = HttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com/resource", + ) + + http_client.request( + path="", + method="GET", + params={"after": "456"}, + request_options=None, + ) + + params = dummy_client.last_request_kwargs["params"] + # For a simple dict, encode_query should give a single (key, value) tuple + assert params == [("after", "456")] + + +@pytest.mark.asyncio +async def test_async_http_client_does_not_pass_empty_params_list() -> None: + """Test that AsyncHttpClient passes params=None when params are empty. + + This prevents httpx from stripping existing query parameters from the URL, + which happens when params=[] or params={} is passed. + """ + dummy_client = _DummyAsyncClient() + http_client = AsyncHttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + async_base_headers=None, + ) + + # Use a path with query params (e.g., pagination cursor URL) + await http_client.request( + path="resource?after=123", + method="GET", + params=None, + request_options=None, + ) + + # We care that httpx receives params=None, not [] or {} + assert "params" in dummy_client.last_request_kwargs + assert dummy_client.last_request_kwargs["params"] is None + + # Verify the query string in the URL is preserved + url = str(dummy_client.last_request_kwargs["url"]) + assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}" + + +@pytest.mark.asyncio +async def test_async_http_client_passes_encoded_params_when_present() -> None: + """Test that AsyncHttpClient passes encoded params when params are provided.""" + dummy_client = _DummyAsyncClient() + http_client = AsyncHttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com/resource", + async_base_headers=None, + ) + + await http_client.request( + path="", + method="GET", + params={"after": "456"}, + request_options=None, + ) + + params = dummy_client.last_request_kwargs["params"] + # For a simple dict, encode_query should give a single (key, value) tuple + assert params == [("after", "456")] + + +def test_basic_url_joining() -> None: + """Test basic URL joining with a simple base URL and path.""" + result = _build_url("https://api.example.com", "/users") + assert result == "https://api.example.com/users" + + +def test_basic_url_joining_trailing_slash() -> None: + """Test basic URL joining with a simple base URL and path.""" + result = _build_url("https://api.example.com/", "/users") + assert result == "https://api.example.com/users" + + +def test_preserves_base_url_path_prefix() -> None: + """Test that path prefixes in base URL are preserved. + + This is the critical bug fix - urllib.parse.urljoin() would strip + the path prefix when the path starts with '/'. + """ + result = _build_url("https://cloud.example.com/org/tenant/api", "/users") + assert result == "https://cloud.example.com/org/tenant/api/users" + + +def test_preserves_base_url_path_prefix_trailing_slash() -> None: + """Test that path prefixes in base URL are preserved.""" + result = _build_url("https://cloud.example.com/org/tenant/api/", "/users") + assert result == "https://cloud.example.com/org/tenant/api/users" + + +# --------------------------------------------------------------------------- +# Connection error retry tests +# --------------------------------------------------------------------------- + + +def _make_sync_http_client(mock_client: Any) -> HttpClient: + return HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + ) + + +def _make_async_http_client(mock_client: Any) -> AsyncHttpClient: + return AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + async_base_headers=None, + ) + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_retries_on_connect_error(mock_sleep: MagicMock) -> None: + """Sync: connection error retries on httpx.ConnectError.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + http_client = _make_sync_http_client(mock_client) + + response = http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_retries_on_remote_protocol_error(mock_sleep: MagicMock) -> None: + """Sync: connection error retries on httpx.RemoteProtocolError.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.RemoteProtocolError("Remote end closed connection without response"), + _DummyResponse(), + ] + http_client = _make_sync_http_client(mock_client) + + response = http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_connection_error_exhausts_retries(mock_sleep: MagicMock) -> None: + """Sync: connection error exhausts retries then raises.""" + mock_client = MagicMock() + mock_client.request.side_effect = httpx.ConnectError("connection failed") + http_client = _make_sync_http_client(mock_client) + + with pytest.raises(httpx.ConnectError): + http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + assert mock_sleep.call_count == 2 + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_connection_error_respects_max_retries_zero(mock_sleep: MagicMock) -> None: + """Sync: connection error respects max_retries=0.""" + mock_client = MagicMock() + mock_client.request.side_effect = httpx.ConnectError("connection failed") + http_client = _make_sync_http_client(mock_client) + + with pytest.raises(httpx.ConnectError): + http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 0}, + ) + + # No retries, just the initial attempt + assert mock_client.request.call_count == 1 + mock_sleep.assert_not_called() + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_retries_on_connect_error(mock_sleep: AsyncMock) -> None: + """Async: connection error retries on httpx.ConnectError.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + ) + http_client = _make_async_http_client(mock_client) + + response = await http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_retries_on_remote_protocol_error(mock_sleep: AsyncMock) -> None: + """Async: connection error retries on httpx.RemoteProtocolError.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.RemoteProtocolError("Remote end closed connection without response"), + _DummyResponse(), + ] + ) + http_client = _make_async_http_client(mock_client) + + response = await http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_connection_error_exhausts_retries(mock_sleep: AsyncMock) -> None: + """Async: connection error exhausts retries then raises.""" + mock_client = MagicMock() + mock_client.request = AsyncMock(side_effect=httpx.ConnectError("connection failed")) + http_client = _make_async_http_client(mock_client) + + with pytest.raises(httpx.ConnectError): + await http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + assert mock_sleep.call_count == 2 + + +# --------------------------------------------------------------------------- +# base_max_retries constructor parameter tests +# --------------------------------------------------------------------------- + + +def test_sync_http_client_default_base_max_retries() -> None: + """HttpClient defaults to base_max_retries=2.""" + http_client = HttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + ) + assert http_client.base_max_retries == 2 + + +def test_async_http_client_default_base_max_retries() -> None: + """AsyncHttpClient defaults to base_max_retries=2.""" + http_client = AsyncHttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + ) + assert http_client.base_max_retries == 2 + + +def test_sync_http_client_custom_base_max_retries() -> None: + """HttpClient accepts a custom base_max_retries value.""" + http_client = HttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_max_retries=5, + ) + assert http_client.base_max_retries == 5 + + +def test_async_http_client_custom_base_max_retries() -> None: + """AsyncHttpClient accepts a custom base_max_retries value.""" + http_client = AsyncHttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_max_retries=5, + ) + assert http_client.base_max_retries == 5 + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_base_max_retries_zero_disables_retries(mock_sleep: MagicMock) -> None: + """Sync: base_max_retries=0 disables retries when no request_options override.""" + mock_client = MagicMock() + mock_client.request.side_effect = httpx.ConnectError("connection failed") + http_client = HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, + ) + + with pytest.raises(httpx.ConnectError): + http_client.request(path="/test", method="GET") + + # No retries, just the initial attempt + assert mock_client.request.call_count == 1 + mock_sleep.assert_not_called() + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_base_max_retries_zero_disables_retries(mock_sleep: AsyncMock) -> None: + """Async: base_max_retries=0 disables retries when no request_options override.""" + mock_client = MagicMock() + mock_client.request = AsyncMock(side_effect=httpx.ConnectError("connection failed")) + http_client = AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, + ) + + with pytest.raises(httpx.ConnectError): + await http_client.request(path="/test", method="GET") + + # No retries, just the initial attempt + assert mock_client.request.call_count == 1 + mock_sleep.assert_not_called() + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_request_options_override_base_max_retries(mock_sleep: MagicMock) -> None: + """Sync: request_options max_retries overrides base_max_retries.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.ConnectError("connection failed"), + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + http_client = HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, # base says no retries + ) + + # But request_options overrides to allow 2 retries + response = http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + assert response.status_code == 200 + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_request_options_override_base_max_retries(mock_sleep: AsyncMock) -> None: + """Async: request_options max_retries overrides base_max_retries.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.ConnectError("connection failed"), + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + ) + http_client = AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, # base says no retries + ) + + # But request_options overrides to allow 2 retries + response = await http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + assert response.status_code == 200 + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_base_max_retries_used_as_default(mock_sleep: MagicMock) -> None: + """Sync: base_max_retries is used when request_options has no max_retries.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + _DummyResponse(), + ] + http_client = HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=3, + ) + + response = http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + # 1 initial + 3 retries = 4 total attempts + assert mock_client.request.call_count == 4 + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_base_max_retries_used_as_default(mock_sleep: AsyncMock) -> None: + """Async: base_max_retries is used when request_options has no max_retries.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + _DummyResponse(), + ] + ) + http_client = AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=3, + ) + + response = await http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + # 1 initial + 3 retries = 4 total attempts + assert mock_client.request.call_count == 4 diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/test_query_encoding.py b/seed/python-sdk/basic-auth-optional/tests/utils/test_query_encoding.py new file mode 100644 index 000000000000..ef5fd7094f9b --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/test_query_encoding.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +from seed.core.query_encoder import encode_query + + +def test_query_encoding_deep_objects() -> None: + assert encode_query({"hello world": "hello world"}) == [("hello world", "hello world")] + assert encode_query({"hello_world": {"hello": "world"}}) == [("hello_world[hello]", "world")] + assert encode_query({"hello_world": {"hello": {"world": "today"}, "test": "this"}, "hi": "there"}) == [ + ("hello_world[hello][world]", "today"), + ("hello_world[test]", "this"), + ("hi", "there"), + ] + + +def test_query_encoding_deep_object_arrays() -> None: + assert encode_query({"objects": [{"key": "hello", "value": "world"}, {"key": "foo", "value": "bar"}]}) == [ + ("objects[key]", "hello"), + ("objects[value]", "world"), + ("objects[key]", "foo"), + ("objects[value]", "bar"), + ] + assert encode_query( + {"users": [{"name": "string", "tags": ["string"]}, {"name": "string2", "tags": ["string2", "string3"]}]} + ) == [ + ("users[name]", "string"), + ("users[tags]", "string"), + ("users[name]", "string2"), + ("users[tags]", "string2"), + ("users[tags]", "string3"), + ] + + +def test_encode_query_with_none() -> None: + encoded = encode_query(None) + assert encoded is None diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/test_serialization.py b/seed/python-sdk/basic-auth-optional/tests/utils/test_serialization.py new file mode 100644 index 000000000000..b298db89c4bd --- /dev/null +++ b/seed/python-sdk/basic-auth-optional/tests/utils/test_serialization.py @@ -0,0 +1,72 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, List + +from .assets.models import ObjectWithOptionalFieldParams, ShapeParams + +from seed.core.serialization import convert_and_respect_annotation_metadata + +UNION_TEST: ShapeParams = {"radius_measurement": 1.0, "shape_type": "circle", "id": "1"} +UNION_TEST_CONVERTED = {"shapeType": "circle", "radiusMeasurement": 1.0, "id": "1"} + + +def test_convert_and_respect_annotation_metadata() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "bool_": True, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" + ) + assert converted == {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"} + + +def test_convert_and_respect_annotation_metadata_in_list() -> None: + data: List[ObjectWithOptionalFieldParams] = [ + {"string": "string", "long_": 12345, "bool_": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long_": 67890, "list_": [], "literal": "lit_one", "any": "any"}, + ] + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=List[ObjectWithOptionalFieldParams], direction="write" + ) + + assert converted == [ + {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long": 67890, "list": [], "literal": "lit_one", "any": "any"}, + ] + + +def test_convert_and_respect_annotation_metadata_in_nested_object() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "union": UNION_TEST, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" + ) + + assert converted == { + "string": "string", + "long": 12345, + "union": UNION_TEST_CONVERTED, + "literal": "lit_one", + "any": "any", + } + + +def test_convert_and_respect_annotation_metadata_in_union() -> None: + converted = convert_and_respect_annotation_metadata(object_=UNION_TEST, annotation=ShapeParams, direction="write") + + assert converted == UNION_TEST_CONVERTED + + +def test_convert_and_respect_annotation_metadata_with_empty_object() -> None: + data: Any = {} + converted = convert_and_respect_annotation_metadata(object_=data, annotation=ShapeParams, direction="write") + assert converted == data diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/api.yml b/test-definitions/fern/apis/basic-auth-optional/definition/api.yml new file mode 100644 index 000000000000..8b1d72b0b769 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/definition/api.yml @@ -0,0 +1,12 @@ +name: basic-auth-optional +auth: Basic +auth-schemes: + Basic: + scheme: basic + username: + name: username + password: + name: password + omit: true +error-discrimination: + strategy: status-code diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml b/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml new file mode 100644 index 000000000000..6a21fd56c9f9 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +imports: + errors: ./errors.yml + +service: + auth: false + base-path: "" + endpoints: + getWithBasicAuth: + auth: true + docs: GET request with basic auth scheme + path: /basic-auth + method: GET + response: + boolean + examples: + - response: + body: true + errors: + - errors.UnauthorizedRequest + + postWithBasicAuth: + auth: true + docs: POST request with basic auth scheme + path: /basic-auth + method: POST + request: + name: PostWithBasicAuth + body: unknown + response: boolean + examples: + - request: + key: "value" + response: + body: true + errors: + - errors.UnauthorizedRequest + - errors.BadRequest diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml b/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml new file mode 100644 index 000000000000..cdd6a9667031 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml @@ -0,0 +1,11 @@ +errors: + UnauthorizedRequest: + status-code: 401 + type: UnauthorizedRequestErrorBody + BadRequest: + status-code: 400 + +types: + UnauthorizedRequestErrorBody: + properties: + message: string diff --git a/test-definitions/fern/apis/basic-auth-optional/generators.yml b/test-definitions/fern/apis/basic-auth-optional/generators.yml new file mode 100644 index 000000000000..b30d6ed97cd2 --- /dev/null +++ b/test-definitions/fern/apis/basic-auth-optional/generators.yml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +groups: + php-sdk: + generators: + - name: fernapi/fern-php-sdk + version: latest + ir-version: v61 + github: + token: ${GITHUB_TOKEN} + mode: push + uri: fern-api/php-sdk-tests + branch: basic-auth-optional + go-sdk: + generators: + - name: fernapi/fern-go-sdk + version: latest + ir-version: v61 + github: + token: ${GITHUB_TOKEN} + mode: push + uri: fern-api/go-sdk-tests + branch: basic-auth-optional From a3f863569a8f288a02c37a82e37aecb2a5985807 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:50:52 +0000 Subject: [PATCH 02/15] fix(python-sdk): use per-field omit checks and constructor optionality instead of coarse eitherOmitted flag Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../client_wrapper_generator.py | 117 +++++++++--------- 1 file changed, 57 insertions(+), 60 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py index 78d1c5da0e66..201954f7703b 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py @@ -523,63 +523,56 @@ def _write_get_headers_body(writer: AST.NodeWriter) -> None: writer.write_newline_if_last_line_not() basic_auth_scheme = self._get_basic_auth_scheme() if basic_auth_scheme is not None: - either_omitted = ( - getattr(basic_auth_scheme, "username_omit", None) is True - or getattr(basic_auth_scheme, "password_omit", None) is True - ) + username_omitted = getattr(basic_auth_scheme, "username_omit", None) is True + password_omitted = getattr(basic_auth_scheme, "password_omit", None) is True + + def _get_username_arg(var_expr: str) -> AST.Expression: + return AST.Expression(f'{var_expr} or ""') if username_omitted else AST.Expression(var_expr) + + def _get_password_arg(var_expr: str) -> AST.Expression: + return AST.Expression(f'{var_expr} or ""') if password_omitted else AST.Expression(var_expr) + if not self._context.ir.sdk_config.is_auth_mandatory: username_var = names.get_username_constructor_parameter_name(basic_auth_scheme) password_var = names.get_password_constructor_parameter_name(basic_auth_scheme) writer.write_line(f"{username_var} = self.{names.get_username_getter_name(basic_auth_scheme)}()") writer.write_line(f"{password_var} = self.{names.get_password_getter_name(basic_auth_scheme)}()") - condition_op = "or" if either_omitted else "and" - writer.write_line(f"if {username_var} is not None {condition_op} {password_var} is not None:") + # Per-field condition: required fields must be present, omittable fields are always satisfied + if not username_omitted and not password_omitted: + condition = f"{username_var} is not None and {password_var} is not None" + elif username_omitted and password_omitted: + condition = f"{username_var} is not None or {password_var} is not None" + elif username_omitted: + condition = f"{password_var} is not None" + else: + condition = f"{username_var} is not None" + writer.write_line(f"if {condition}:") with writer.indent(): writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') - if either_omitted: - writer.write_node( - AST.ClassInstantiation( - class_=httpx.HttpX.BASIC_AUTH, - args=[ - AST.Expression(f'{username_var} or ""'), - AST.Expression(f'{password_var} or ""'), - ], - ) - ) - else: - writer.write_node( - AST.ClassInstantiation( - class_=httpx.HttpX.BASIC_AUTH, - args=[ - AST.Expression(f"{username_var}"), - AST.Expression(f"{password_var}"), - ], - ) - ) - writer.write("._auth_header") - writer.write_newline_if_last_line_not() - else: - writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') - if either_omitted: writer.write_node( AST.ClassInstantiation( class_=httpx.HttpX.BASIC_AUTH, args=[ - AST.Expression(f'self.{names.get_username_getter_name(basic_auth_scheme)}() or ""'), - AST.Expression(f'self.{names.get_password_getter_name(basic_auth_scheme)}() or ""'), + _get_username_arg(username_var), + _get_password_arg(password_var), ], ) ) - else: - writer.write_node( - AST.ClassInstantiation( - class_=httpx.HttpX.BASIC_AUTH, - args=[ - AST.Expression(f"self.{names.get_username_getter_name(basic_auth_scheme)}()"), - AST.Expression(f"self.{names.get_password_getter_name(basic_auth_scheme)}()"), - ], - ) + writer.write("._auth_header") + writer.write_newline_if_last_line_not() + else: + writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') + username_getter = f"self.{names.get_username_getter_name(basic_auth_scheme)}()" + password_getter = f"self.{names.get_password_getter_name(basic_auth_scheme)}()" + writer.write_node( + AST.ClassInstantiation( + class_=httpx.HttpX.BASIC_AUTH, + args=[ + _get_username_arg(username_getter), + _get_password_arg(password_getter), + ], ) + ) writer.write("._auth_header") writer.write_newline_if_last_line_not() for param in constructor_parameters: @@ -829,14 +822,17 @@ def _get_constructor_info(self, exclude_auth: bool = False) -> ConstructorInfo: basic_auth_scheme = self._get_basic_auth_scheme() if basic_auth_scheme is not None: + username_omitted = getattr(basic_auth_scheme, "username_omit", None) is True + password_omitted = getattr(basic_auth_scheme, "password_omit", None) is True username_constructor_parameter_name = names.get_username_constructor_parameter_name(basic_auth_scheme) + username_is_optional = not self._context.ir.sdk_config.is_auth_mandatory or username_omitted username_constructor_parameter = ConstructorParameter( constructor_parameter_name=username_constructor_parameter_name, private_member_name=names.get_username_member_name(basic_auth_scheme), type_hint=( - ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT - if self._context.ir.sdk_config.is_auth_mandatory - else AST.TypeHint.optional(ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT) + AST.TypeHint.optional(ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT) + if username_is_optional + else ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT ), initializer=AST.Expression( f'{username_constructor_parameter_name}="YOUR_{basic_auth_scheme.username.screaming_snake_case.safe_name}"', @@ -846,17 +842,17 @@ def _get_constructor_info(self, exclude_auth: bool = False) -> ConstructorInfo: signature=AST.FunctionSignature( parameters=[], return_type=( - AST.TypeHint.str_() - if self._context.ir.sdk_config.is_auth_mandatory - else AST.TypeHint.optional(AST.TypeHint.str_()) + AST.TypeHint.optional(AST.TypeHint.str_()) + if username_is_optional + else AST.TypeHint.str_() ), ), body=AST.CodeWriter( - self._get_required_getter_body_writer( + self._get_optional_getter_body_writer( member_name=names.get_username_member_name(basic_auth_scheme) ) - if self._context.ir.sdk_config.is_auth_mandatory - else self._get_optional_getter_body_writer( + if username_is_optional + else self._get_required_getter_body_writer( member_name=names.get_username_member_name(basic_auth_scheme) ) ), @@ -879,13 +875,14 @@ def _get_constructor_info(self, exclude_auth: bool = False) -> ConstructorInfo: ), ) password_constructor_parameter_name = names.get_password_constructor_parameter_name(basic_auth_scheme) + password_is_optional = not self._context.ir.sdk_config.is_auth_mandatory or password_omitted password_constructor_parameter = ConstructorParameter( constructor_parameter_name=password_constructor_parameter_name, private_member_name=names.get_password_member_name(basic_auth_scheme), type_hint=( - ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT - if self._context.ir.sdk_config.is_auth_mandatory - else AST.TypeHint.optional(ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT) + AST.TypeHint.optional(ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT) + if password_is_optional + else ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT ), initializer=AST.Expression( f'{password_constructor_parameter_name}="YOUR_{basic_auth_scheme.password.screaming_snake_case.safe_name}"', @@ -895,17 +892,17 @@ def _get_constructor_info(self, exclude_auth: bool = False) -> ConstructorInfo: signature=AST.FunctionSignature( parameters=[], return_type=( - AST.TypeHint.str_() - if self._context.ir.sdk_config.is_auth_mandatory - else AST.TypeHint.optional(AST.TypeHint.str_()) + AST.TypeHint.optional(AST.TypeHint.str_()) + if password_is_optional + else AST.TypeHint.str_() ), ), body=AST.CodeWriter( - self._get_required_getter_body_writer( + self._get_optional_getter_body_writer( member_name=names.get_password_member_name(basic_auth_scheme) ) - if self._context.ir.sdk_config.is_auth_mandatory - else self._get_optional_getter_body_writer( + if password_is_optional + else self._get_required_getter_body_writer( member_name=names.get_password_member_name(basic_auth_scheme) ) ), From e388f2cf31e33186e25b3365da10f65833741f68 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:11:42 +0000 Subject: [PATCH 03/15] fix(python-sdk): regenerate seed output for basic-auth-optional after per-field omit fix Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../basic-auth-optional/src/seed/client.py | 8 ++++---- .../src/seed/core/client_wrapper.py | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/seed/python-sdk/basic-auth-optional/src/seed/client.py b/seed/python-sdk/basic-auth-optional/src/seed/client.py index a2c698bd1d06..ddc010416bd5 100644 --- a/seed/python-sdk/basic-auth-optional/src/seed/client.py +++ b/seed/python-sdk/basic-auth-optional/src/seed/client.py @@ -22,7 +22,7 @@ class SeedBasicAuthOptional: The base url to use for requests from the client. username : typing.Union[str, typing.Callable[[], str]] - password : typing.Union[str, typing.Callable[[], str]] + password : typing.Optional[typing.Union[str, typing.Callable[[], str]]] headers : typing.Optional[typing.Dict[str, str]] Additional headers to send with every request. @@ -54,7 +54,7 @@ def __init__( *, base_url: str, username: typing.Union[str, typing.Callable[[], str]], - password: typing.Union[str, typing.Callable[[], str]], + password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, @@ -98,7 +98,7 @@ class AsyncSeedBasicAuthOptional: The base url to use for requests from the client. username : typing.Union[str, typing.Callable[[], str]] - password : typing.Union[str, typing.Callable[[], str]] + password : typing.Optional[typing.Union[str, typing.Callable[[], str]]] headers : typing.Optional[typing.Dict[str, str]] Additional headers to send with every request. @@ -130,7 +130,7 @@ def __init__( *, base_url: str, username: typing.Union[str, typing.Callable[[], str]], - password: typing.Union[str, typing.Callable[[], str]], + password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, follow_redirects: typing.Optional[bool] = True, diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/client_wrapper.py b/seed/python-sdk/basic-auth-optional/src/seed/core/client_wrapper.py index 16988c81f98c..d364f6d1f7bd 100644 --- a/seed/python-sdk/basic-auth-optional/src/seed/core/client_wrapper.py +++ b/seed/python-sdk/basic-auth-optional/src/seed/core/client_wrapper.py @@ -12,7 +12,7 @@ def __init__( self, *, username: typing.Union[str, typing.Callable[[], str]], - password: typing.Union[str, typing.Callable[[], str]], + password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, @@ -37,7 +37,7 @@ def get_headers(self) -> typing.Dict[str, str]: "X-Fern-SDK-Version": "0.0.1", **(self.get_custom_headers() or {}), } - headers["Authorization"] = httpx.BasicAuth(self._get_username() or "", self._get_password() or "")._auth_header + headers["Authorization"] = httpx.BasicAuth(self._get_username(), self._get_password() or "")._auth_header return headers def _get_username(self) -> str: @@ -46,8 +46,8 @@ def _get_username(self) -> str: else: return self._username() - def _get_password(self) -> str: - if isinstance(self._password, str): + def _get_password(self) -> typing.Optional[str]: + if isinstance(self._password, str) or self._password is None: return self._password else: return self._password() @@ -67,7 +67,7 @@ def __init__( self, *, username: typing.Union[str, typing.Callable[[], str]], - password: typing.Union[str, typing.Callable[[], str]], + password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, @@ -91,7 +91,7 @@ def __init__( self, *, username: typing.Union[str, typing.Callable[[], str]], - password: typing.Union[str, typing.Callable[[], str]], + password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, From 5167926f4606eb62c1d7d397bf6a06e8912c23d0 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:48:57 +0000 Subject: [PATCH 04/15] fix(python-sdk): apply ruff-format formatting fixes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/core_utilities/client_wrapper_generator.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py index 201954f7703b..7cfd6a2f75a8 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py @@ -842,9 +842,7 @@ def _get_constructor_info(self, exclude_auth: bool = False) -> ConstructorInfo: signature=AST.FunctionSignature( parameters=[], return_type=( - AST.TypeHint.optional(AST.TypeHint.str_()) - if username_is_optional - else AST.TypeHint.str_() + AST.TypeHint.optional(AST.TypeHint.str_()) if username_is_optional else AST.TypeHint.str_() ), ), body=AST.CodeWriter( @@ -892,9 +890,7 @@ def _get_constructor_info(self, exclude_auth: bool = False) -> ConstructorInfo: signature=AST.FunctionSignature( parameters=[], return_type=( - AST.TypeHint.optional(AST.TypeHint.str_()) - if password_is_optional - else AST.TypeHint.str_() + AST.TypeHint.optional(AST.TypeHint.str_()) if password_is_optional else AST.TypeHint.str_() ), ), body=AST.CodeWriter( From af66447bb4e53b5470a88e769bf56876116a9e89 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Thu, 2 Apr 2026 00:12:03 +0000 Subject: [PATCH 05/15] fix(python-sdk): remove omitted fields entirely from constructor params, use empty string internally Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../client_wrapper_generator.py | 254 +++++++++--------- 1 file changed, 132 insertions(+), 122 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py index 7cfd6a2f75a8..c0855e4c2286 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py @@ -526,50 +526,56 @@ def _write_get_headers_body(writer: AST.NodeWriter) -> None: username_omitted = getattr(basic_auth_scheme, "username_omit", None) is True password_omitted = getattr(basic_auth_scheme, "password_omit", None) is True - def _get_username_arg(var_expr: str) -> AST.Expression: - return AST.Expression(f'{var_expr} or ""') if username_omitted else AST.Expression(var_expr) - - def _get_password_arg(var_expr: str) -> AST.Expression: - return AST.Expression(f'{var_expr} or ""') if password_omitted else AST.Expression(var_expr) - if not self._context.ir.sdk_config.is_auth_mandatory: - username_var = names.get_username_constructor_parameter_name(basic_auth_scheme) - password_var = names.get_password_constructor_parameter_name(basic_auth_scheme) - writer.write_line(f"{username_var} = self.{names.get_username_getter_name(basic_auth_scheme)}()") - writer.write_line(f"{password_var} = self.{names.get_password_getter_name(basic_auth_scheme)}()") - # Per-field condition: required fields must be present, omittable fields are always satisfied - if not username_omitted and not password_omitted: - condition = f"{username_var} is not None and {password_var} is not None" - elif username_omitted and password_omitted: - condition = f"{username_var} is not None or {password_var} is not None" - elif username_omitted: - condition = f"{password_var} is not None" + # Build condition and args based on which fields are omitted vs present + conditions = [] + if not username_omitted: + username_var = names.get_username_constructor_parameter_name(basic_auth_scheme) + writer.write_line(f"{username_var} = self.{names.get_username_getter_name(basic_auth_scheme)}()") + conditions.append(f"{username_var} is not None") + if not password_omitted: + password_var = names.get_password_constructor_parameter_name(basic_auth_scheme) + writer.write_line(f"{password_var} = self.{names.get_password_getter_name(basic_auth_scheme)}()") + conditions.append(f"{password_var} is not None") + + # Omitted fields use empty string directly + username_arg = AST.Expression('""') if username_omitted else AST.Expression(username_var) + password_arg = AST.Expression('""') if password_omitted else AST.Expression(password_var) + + if conditions: + writer.write_line(f"if {' and '.join(conditions)}:") + with writer.indent(): + writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') + writer.write_node( + AST.ClassInstantiation( + class_=httpx.HttpX.BASIC_AUTH, + args=[username_arg, password_arg], + ) + ) + writer.write("._auth_header") + writer.write_newline_if_last_line_not() else: - condition = f"{username_var} is not None" - writer.write_line(f"if {condition}:") - with writer.indent(): + # Both fields omitted - always set header with empty strings writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') writer.write_node( AST.ClassInstantiation( class_=httpx.HttpX.BASIC_AUTH, - args=[ - _get_username_arg(username_var), - _get_password_arg(password_var), - ], + args=[username_arg, password_arg], ) ) writer.write("._auth_header") writer.write_newline_if_last_line_not() else: + # Auth is mandatory - omitted fields use empty string + username_getter = '""' if username_omitted else f"self.{names.get_username_getter_name(basic_auth_scheme)}()" + password_getter = '""' if password_omitted else f"self.{names.get_password_getter_name(basic_auth_scheme)}()" writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') - username_getter = f"self.{names.get_username_getter_name(basic_auth_scheme)}()" - password_getter = f"self.{names.get_password_getter_name(basic_auth_scheme)}()" writer.write_node( AST.ClassInstantiation( class_=httpx.HttpX.BASIC_AUTH, args=[ - _get_username_arg(username_getter), - _get_password_arg(password_getter), + AST.Expression(username_getter), + AST.Expression(password_getter), ], ) ) @@ -824,108 +830,112 @@ def _get_constructor_info(self, exclude_auth: bool = False) -> ConstructorInfo: if basic_auth_scheme is not None: username_omitted = getattr(basic_auth_scheme, "username_omit", None) is True password_omitted = getattr(basic_auth_scheme, "password_omit", None) is True - username_constructor_parameter_name = names.get_username_constructor_parameter_name(basic_auth_scheme) - username_is_optional = not self._context.ir.sdk_config.is_auth_mandatory or username_omitted - username_constructor_parameter = ConstructorParameter( - constructor_parameter_name=username_constructor_parameter_name, - private_member_name=names.get_username_member_name(basic_auth_scheme), - type_hint=( - AST.TypeHint.optional(ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT) - if username_is_optional - else ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT - ), - initializer=AST.Expression( - f'{username_constructor_parameter_name}="YOUR_{basic_auth_scheme.username.screaming_snake_case.safe_name}"', - ), - getter_method=AST.FunctionDeclaration( - name=names.get_username_getter_name(basic_auth_scheme), - signature=AST.FunctionSignature( - parameters=[], - return_type=( - AST.TypeHint.optional(AST.TypeHint.str_()) if username_is_optional else AST.TypeHint.str_() - ), + + # When omit is true, the field is completely removed from the end-user API. + # Only add non-omitted fields to constructor parameters. + if not username_omitted: + username_constructor_parameter_name = names.get_username_constructor_parameter_name(basic_auth_scheme) + username_constructor_parameter = ConstructorParameter( + constructor_parameter_name=username_constructor_parameter_name, + private_member_name=names.get_username_member_name(basic_auth_scheme), + type_hint=( + ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT + if self._context.ir.sdk_config.is_auth_mandatory + else AST.TypeHint.optional(ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT) ), - body=AST.CodeWriter( - self._get_optional_getter_body_writer( - member_name=names.get_username_member_name(basic_auth_scheme) - ) - if username_is_optional - else self._get_required_getter_body_writer( - member_name=names.get_username_member_name(basic_auth_scheme) - ) + initializer=AST.Expression( + f'{username_constructor_parameter_name}="YOUR_{basic_auth_scheme.username.screaming_snake_case.safe_name}"', ), - ), - environment_variable=( - basic_auth_scheme.username_env_var if basic_auth_scheme.username_env_var is not None else None - ), - is_basic=True, - template=TemplateGenerator.string_template( - is_optional=False, - template_string_prefix=username_constructor_parameter_name, - inputs=[ - TemplateInput.factory.payload( - PayloadInput( - location="AUTH", - path="username", - ) + getter_method=AST.FunctionDeclaration( + name=names.get_username_getter_name(basic_auth_scheme), + signature=AST.FunctionSignature( + parameters=[], + return_type=( + AST.TypeHint.str_() + if self._context.ir.sdk_config.is_auth_mandatory + else AST.TypeHint.optional(AST.TypeHint.str_()) + ), ), - ], - ), - ) - password_constructor_parameter_name = names.get_password_constructor_parameter_name(basic_auth_scheme) - password_is_optional = not self._context.ir.sdk_config.is_auth_mandatory or password_omitted - password_constructor_parameter = ConstructorParameter( - constructor_parameter_name=password_constructor_parameter_name, - private_member_name=names.get_password_member_name(basic_auth_scheme), - type_hint=( - AST.TypeHint.optional(ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT) - if password_is_optional - else ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT - ), - initializer=AST.Expression( - f'{password_constructor_parameter_name}="YOUR_{basic_auth_scheme.password.screaming_snake_case.safe_name}"', - ), - getter_method=AST.FunctionDeclaration( - name=names.get_password_getter_name(basic_auth_scheme), - signature=AST.FunctionSignature( - parameters=[], - return_type=( - AST.TypeHint.optional(AST.TypeHint.str_()) if password_is_optional else AST.TypeHint.str_() + body=AST.CodeWriter( + self._get_required_getter_body_writer( + member_name=names.get_username_member_name(basic_auth_scheme) + ) + if self._context.ir.sdk_config.is_auth_mandatory + else self._get_optional_getter_body_writer( + member_name=names.get_username_member_name(basic_auth_scheme) + ) ), ), - body=AST.CodeWriter( - self._get_optional_getter_body_writer( - member_name=names.get_password_member_name(basic_auth_scheme) - ) - if password_is_optional - else self._get_required_getter_body_writer( - member_name=names.get_password_member_name(basic_auth_scheme) - ) + environment_variable=( + basic_auth_scheme.username_env_var if basic_auth_scheme.username_env_var is not None else None ), - ), - is_basic=True, - environment_variable=( - basic_auth_scheme.password_env_var if basic_auth_scheme.password_env_var is not None else None - ), - template=TemplateGenerator.string_template( - is_optional=False, - template_string_prefix=password_constructor_parameter_name, - inputs=[ - TemplateInput.factory.payload( - PayloadInput( - location="AUTH", - path="password", + is_basic=True, + template=TemplateGenerator.string_template( + is_optional=False, + template_string_prefix=username_constructor_parameter_name, + inputs=[ + TemplateInput.factory.payload( + PayloadInput( + location="AUTH", + path="username", + ) + ), + ], + ), + ) + parameters.append(username_constructor_parameter) + + if not password_omitted: + password_constructor_parameter_name = names.get_password_constructor_parameter_name(basic_auth_scheme) + password_constructor_parameter = ConstructorParameter( + constructor_parameter_name=password_constructor_parameter_name, + private_member_name=names.get_password_member_name(basic_auth_scheme), + type_hint=( + ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT + if self._context.ir.sdk_config.is_auth_mandatory + else AST.TypeHint.optional(ClientWrapperGenerator.STRING_OR_SUPPLIER_TYPE_HINT) + ), + initializer=AST.Expression( + f'{password_constructor_parameter_name}="YOUR_{basic_auth_scheme.password.screaming_snake_case.safe_name}"', + ), + getter_method=AST.FunctionDeclaration( + name=names.get_password_getter_name(basic_auth_scheme), + signature=AST.FunctionSignature( + parameters=[], + return_type=( + AST.TypeHint.str_() + if self._context.ir.sdk_config.is_auth_mandatory + else AST.TypeHint.optional(AST.TypeHint.str_()) + ), + ), + body=AST.CodeWriter( + self._get_required_getter_body_writer( + member_name=names.get_password_member_name(basic_auth_scheme) + ) + if self._context.ir.sdk_config.is_auth_mandatory + else self._get_optional_getter_body_writer( + member_name=names.get_password_member_name(basic_auth_scheme) ) ), - ], - ), - ) - parameters.extend( - [ - username_constructor_parameter, - password_constructor_parameter, - ] - ) + ), + is_basic=True, + environment_variable=( + basic_auth_scheme.password_env_var if basic_auth_scheme.password_env_var is not None else None + ), + template=TemplateGenerator.string_template( + is_optional=False, + template_string_prefix=password_constructor_parameter_name, + inputs=[ + TemplateInput.factory.payload( + PayloadInput( + location="AUTH", + path="password", + ) + ), + ], + ), + ) + parameters.append(password_constructor_parameter) # Add generic headers parameter parameters.append(headers_constructor_parameter) From d447854ce44670e1f94c9f7b0d3d21ee7a435396 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Thu, 2 Apr 2026 01:14:12 +0000 Subject: [PATCH 06/15] fix(python-sdk): apply ruff-format to client_wrapper_generator.py Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../core_utilities/client_wrapper_generator.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py index c0855e4c2286..19cd1ad279c8 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py @@ -531,11 +531,15 @@ def _write_get_headers_body(writer: AST.NodeWriter) -> None: conditions = [] if not username_omitted: username_var = names.get_username_constructor_parameter_name(basic_auth_scheme) - writer.write_line(f"{username_var} = self.{names.get_username_getter_name(basic_auth_scheme)}()") + writer.write_line( + f"{username_var} = self.{names.get_username_getter_name(basic_auth_scheme)}()" + ) conditions.append(f"{username_var} is not None") if not password_omitted: password_var = names.get_password_constructor_parameter_name(basic_auth_scheme) - writer.write_line(f"{password_var} = self.{names.get_password_getter_name(basic_auth_scheme)}()") + writer.write_line( + f"{password_var} = self.{names.get_password_getter_name(basic_auth_scheme)}()" + ) conditions.append(f"{password_var} is not None") # Omitted fields use empty string directly @@ -567,8 +571,12 @@ def _write_get_headers_body(writer: AST.NodeWriter) -> None: writer.write_newline_if_last_line_not() else: # Auth is mandatory - omitted fields use empty string - username_getter = '""' if username_omitted else f"self.{names.get_username_getter_name(basic_auth_scheme)}()" - password_getter = '""' if password_omitted else f"self.{names.get_password_getter_name(basic_auth_scheme)}()" + username_getter = ( + '""' if username_omitted else f"self.{names.get_username_getter_name(basic_auth_scheme)}()" + ) + password_getter = ( + '""' if password_omitted else f"self.{names.get_password_getter_name(basic_auth_scheme)}()" + ) writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') writer.write_node( AST.ClassInstantiation( From 55b8f5522fea6d622412f5d00675fb64af1a37fd Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:19:37 +0000 Subject: [PATCH 07/15] fix(python-sdk): skip auth header when both fields omitted and auth is non-mandatory Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/core_utilities/client_wrapper_generator.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py index 19cd1ad279c8..1b196be4d53b 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/client_wrapper_generator.py @@ -559,16 +559,8 @@ def _write_get_headers_body(writer: AST.NodeWriter) -> None: writer.write("._auth_header") writer.write_newline_if_last_line_not() else: - # Both fields omitted - always set header with empty strings - writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') - writer.write_node( - AST.ClassInstantiation( - class_=httpx.HttpX.BASIC_AUTH, - args=[username_arg, password_arg], - ) - ) - writer.write("._auth_header") - writer.write_newline_if_last_line_not() + # Both fields omitted and auth is non-mandatory - skip header entirely + pass else: # Auth is mandatory - omitted fields use empty string username_getter = ( From 0e7aed71e7f6a8477c9524da23295c096d749dc6 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:16:22 +0000 Subject: [PATCH 08/15] fix(python-sdk): use 'omit' instead of 'optional' in versions.yml changelog Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/python/sdk/versions.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/generators/python/sdk/versions.yml b/generators/python/sdk/versions.yml index 4ef0d832b2ba..4654e1aee08c 100644 --- a/generators/python/sdk/versions.yml +++ b/generators/python/sdk/versions.yml @@ -3,11 +3,11 @@ - version: 5.2.2 changelogEntry: - summary: | - Support optional username and password in basic auth when configured in IR. - When usernameOmit or passwordOmit is set, the SDK accepts username-only, - password-only, or both credentials. Missing fields are treated as empty strings - (e.g., username-only encodes `username:`, password-only encodes `:password`). - When neither is provided, the Authorization header is omitted entirely. + Support omitting username or password from basic auth when configured via + `usernameOmit` or `passwordOmit` in the IR. Omitted fields are removed from + the SDK's public API and treated as empty strings internally (e.g., omitting + password encodes `username:`, omitting username encodes `:password`). When + both are omitted, the Authorization header is skipped entirely. type: feat createdAt: "2026-03-31" irVersion: 65 From dea51d21e53c93142ba6e9c4f2f74794cf33e65e Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:34:54 +0000 Subject: [PATCH 09/15] refactor: rename basic-auth-optional fixture to basic-auth-pw-omitted Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ...e_errors_UnauthorizedRequestErrorBody.json | 0 .../basic-auth-optional/snippet.json | 31 ------------------- .../basic-auth-optional/src/seed/version.py | 3 -- .../.fern/metadata.json | 0 .../.github/workflows/ci.yml | 0 .../.gitignore | 0 .../README.md | 24 +++++++------- .../poetry.lock | 0 .../pyproject.toml | 6 ++-- .../reference.md | 8 ++--- .../requirements.txt | 0 .../basic-auth-pw-omitted/snippet.json | 31 +++++++++++++++++++ .../src/seed/__init__.py | 10 +++--- .../src/seed/basic_auth/__init__.py | 0 .../src/seed/basic_auth/client.py | 16 +++++----- .../src/seed/basic_auth/raw_client.py | 0 .../src/seed/client.py | 12 +++---- .../src/seed/core/__init__.py | 0 .../src/seed/core/api_error.py | 0 .../src/seed/core/client_wrapper.py | 4 +-- .../src/seed/core/datetime_utils.py | 0 .../src/seed/core/file.py | 0 .../src/seed/core/force_multipart.py | 0 .../src/seed/core/http_client.py | 0 .../src/seed/core/http_response.py | 0 .../src/seed/core/http_sse/__init__.py | 0 .../src/seed/core/http_sse/_api.py | 0 .../src/seed/core/http_sse/_decoders.py | 0 .../src/seed/core/http_sse/_exceptions.py | 0 .../src/seed/core/http_sse/_models.py | 0 .../src/seed/core/jsonable_encoder.py | 0 .../src/seed/core/logging.py | 0 .../src/seed/core/parse_error.py | 0 .../src/seed/core/pydantic_utilities.py | 0 .../src/seed/core/query_encoder.py | 0 .../src/seed/core/remove_none_from_dict.py | 0 .../src/seed/core/request_options.py | 0 .../src/seed/core/serialization.py | 0 .../src/seed/errors/__init__.py | 0 .../src/seed/errors/errors/__init__.py | 0 .../src/seed/errors/errors/bad_request.py | 0 .../errors/errors/unauthorized_request.py | 0 .../src/seed/errors/types/__init__.py | 0 .../types/unauthorized_request_error_body.py | 0 .../src/seed/py.typed | 0 .../basic-auth-pw-omitted/src/seed/version.py | 3 ++ .../tests/custom/test_client.py | 0 .../tests/utils/__init__.py | 0 .../tests/utils/assets/models/__init__.py | 0 .../tests/utils/assets/models/circle.py | 0 .../tests/utils/assets/models/color.py | 0 .../assets/models/object_with_defaults.py | 0 .../models/object_with_optional_field.py | 0 .../tests/utils/assets/models/shape.py | 0 .../tests/utils/assets/models/square.py | 0 .../assets/models/undiscriminated_shape.py | 0 .../tests/utils/test_http_client.py | 0 .../tests/utils/test_query_encoding.py | 0 .../tests/utils/test_serialization.py | 0 .../definition/api.yml | 2 +- .../definition/basic-auth.yml | 0 .../definition/errors.yml | 0 .../generators.yml | 4 +-- 63 files changed, 77 insertions(+), 77 deletions(-) rename packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/{basic-auth-optional => basic-auth-pw-omitted}/type_errors_UnauthorizedRequestErrorBody.json (100%) delete mode 100644 seed/python-sdk/basic-auth-optional/snippet.json delete mode 100644 seed/python-sdk/basic-auth-optional/src/seed/version.py rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/.fern/metadata.json (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/.github/workflows/ci.yml (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/.gitignore (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/README.md (88%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/poetry.lock (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/pyproject.toml (94%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/reference.md (90%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/requirements.txt (100%) create mode 100644 seed/python-sdk/basic-auth-pw-omitted/snippet.json rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/__init__.py (86%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/basic_auth/__init__.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/basic_auth/client.py (92%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/basic_auth/raw_client.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/client.py (96%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/__init__.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/api_error.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/client_wrapper.py (97%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/datetime_utils.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/file.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/force_multipart.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/http_client.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/http_response.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/http_sse/__init__.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/http_sse/_api.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/http_sse/_decoders.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/http_sse/_models.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/jsonable_encoder.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/logging.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/parse_error.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/pydantic_utilities.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/query_encoder.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/remove_none_from_dict.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/request_options.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/core/serialization.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/errors/__init__.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/errors/errors/__init__.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/errors/errors/bad_request.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/errors/errors/unauthorized_request.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/errors/types/__init__.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/errors/types/unauthorized_request_error_body.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/src/seed/py.typed (100%) create mode 100644 seed/python-sdk/basic-auth-pw-omitted/src/seed/version.py rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/custom/test_client.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/__init__.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/assets/models/__init__.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/assets/models/circle.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/assets/models/color.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/assets/models/object_with_defaults.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/assets/models/object_with_optional_field.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/assets/models/shape.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/assets/models/square.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/assets/models/undiscriminated_shape.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/test_http_client.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/test_query_encoding.py (100%) rename seed/python-sdk/{basic-auth-optional => basic-auth-pw-omitted}/tests/utils/test_serialization.py (100%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/definition/api.yml (86%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/definition/basic-auth.yml (100%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/definition/errors.yml (100%) rename test-definitions/fern/apis/{basic-auth-optional => basic-auth-pw-omitted}/generators.yml (86%) diff --git a/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json b/packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-pw-omitted/type_errors_UnauthorizedRequestErrorBody.json similarity index 100% rename from packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-optional/type_errors_UnauthorizedRequestErrorBody.json rename to packages/cli/fern-definition/ir-to-jsonschema/src/__test__/__snapshots__/basic-auth-pw-omitted/type_errors_UnauthorizedRequestErrorBody.json diff --git a/seed/python-sdk/basic-auth-optional/snippet.json b/seed/python-sdk/basic-auth-optional/snippet.json deleted file mode 100644 index 2bbf238b62a5..000000000000 --- a/seed/python-sdk/basic-auth-optional/snippet.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "types": {}, - "endpoints": [ - { - "example_identifier": "default", - "id": { - "path": "/basic-auth", - "method": "GET", - "identifier_override": "endpoint_basic-auth.getWithBasicAuth" - }, - "snippet": { - "sync_client": "from seed import SeedBasicAuthOptional\n\nclient = SeedBasicAuthOptional(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.basic_auth.get_with_basic_auth()\n", - "async_client": "import asyncio\n\nfrom seed import AsyncSeedBasicAuthOptional\n\nclient = AsyncSeedBasicAuthOptional(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.basic_auth.get_with_basic_auth()\n\n\nasyncio.run(main())\n", - "type": "python" - } - }, - { - "example_identifier": "default", - "id": { - "path": "/basic-auth", - "method": "POST", - "identifier_override": "endpoint_basic-auth.postWithBasicAuth" - }, - "snippet": { - "sync_client": "from seed import SeedBasicAuthOptional\n\nclient = SeedBasicAuthOptional(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.basic_auth.post_with_basic_auth(\n request={\"key\": \"value\"},\n)\n", - "async_client": "import asyncio\n\nfrom seed import AsyncSeedBasicAuthOptional\n\nclient = AsyncSeedBasicAuthOptional(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.basic_auth.post_with_basic_auth(\n request={\"key\": \"value\"},\n )\n\n\nasyncio.run(main())\n", - "type": "python" - } - } - ] -} \ No newline at end of file diff --git a/seed/python-sdk/basic-auth-optional/src/seed/version.py b/seed/python-sdk/basic-auth-optional/src/seed/version.py deleted file mode 100644 index b8bd9635d24c..000000000000 --- a/seed/python-sdk/basic-auth-optional/src/seed/version.py +++ /dev/null @@ -1,3 +0,0 @@ -from importlib import metadata - -__version__ = metadata.version("fern_basic-auth-optional") diff --git a/seed/python-sdk/basic-auth-optional/.fern/metadata.json b/seed/python-sdk/basic-auth-pw-omitted/.fern/metadata.json similarity index 100% rename from seed/python-sdk/basic-auth-optional/.fern/metadata.json rename to seed/python-sdk/basic-auth-pw-omitted/.fern/metadata.json diff --git a/seed/python-sdk/basic-auth-optional/.github/workflows/ci.yml b/seed/python-sdk/basic-auth-pw-omitted/.github/workflows/ci.yml similarity index 100% rename from seed/python-sdk/basic-auth-optional/.github/workflows/ci.yml rename to seed/python-sdk/basic-auth-pw-omitted/.github/workflows/ci.yml diff --git a/seed/python-sdk/basic-auth-optional/.gitignore b/seed/python-sdk/basic-auth-pw-omitted/.gitignore similarity index 100% rename from seed/python-sdk/basic-auth-optional/.gitignore rename to seed/python-sdk/basic-auth-pw-omitted/.gitignore diff --git a/seed/python-sdk/basic-auth-optional/README.md b/seed/python-sdk/basic-auth-pw-omitted/README.md similarity index 88% rename from seed/python-sdk/basic-auth-optional/README.md rename to seed/python-sdk/basic-auth-pw-omitted/README.md index 9bb342b7a6d3..d0d816bcd3d7 100644 --- a/seed/python-sdk/basic-auth-optional/README.md +++ b/seed/python-sdk/basic-auth-pw-omitted/README.md @@ -1,7 +1,7 @@ # Seed Python Library [![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FPython) -[![pypi](https://img.shields.io/pypi/v/fern_basic-auth-optional)](https://pypi.python.org/pypi/fern_basic-auth-optional) +[![pypi](https://img.shields.io/pypi/v/fern_basic-auth-pw-omitted)](https://pypi.python.org/pypi/fern_basic-auth-pw-omitted) The Seed Python library provides convenient access to the Seed APIs from Python. @@ -22,7 +22,7 @@ The Seed Python library provides convenient access to the Seed APIs from Python. ## Installation ```sh -pip install fern_basic-auth-optional +pip install fern_basic-auth-pw-omitted ``` ## Reference @@ -34,9 +34,9 @@ A full reference for this library is available [here](./reference.md). Instantiate and use the client with the following: ```python -from seed import SeedBasicAuthOptional +from seed import SeedBasicAuthPwOmitted -client = SeedBasicAuthOptional( +client = SeedBasicAuthPwOmitted( username="", password="", base_url="https://yourhost.com/path/to/api", @@ -54,9 +54,9 @@ The SDK also exports an `async` client so that you can make non-blocking calls t ```python import asyncio -from seed import AsyncSeedBasicAuthOptional +from seed import AsyncSeedBasicAuthPwOmitted -client = AsyncSeedBasicAuthOptional( +client = AsyncSeedBasicAuthPwOmitted( username="", password="", base_url="https://yourhost.com/path/to/api", @@ -95,9 +95,9 @@ The SDK provides access to raw response data, including headers, through the `.w The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes. ```python -from seed import SeedBasicAuthOptional +from seed import SeedBasicAuthPwOmitted -client = SeedBasicAuthOptional(...) +client = SeedBasicAuthPwOmitted(...) response = client.basic_auth.with_raw_response.post_with_basic_auth(...) print(response.headers) # access the response headers print(response.status_code) # access the response status code @@ -129,9 +129,9 @@ client.basic_auth.post_with_basic_auth(..., request_options={ The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. ```python -from seed import SeedBasicAuthOptional +from seed import SeedBasicAuthPwOmitted -client = SeedBasicAuthOptional(..., timeout=20.0) +client = SeedBasicAuthPwOmitted(..., timeout=20.0) # Override timeout for a specific method client.basic_auth.post_with_basic_auth(..., request_options={ @@ -146,9 +146,9 @@ and transports. ```python import httpx -from seed import SeedBasicAuthOptional +from seed import SeedBasicAuthPwOmitted -client = SeedBasicAuthOptional( +client = SeedBasicAuthPwOmitted( ..., httpx_client=httpx.Client( proxy="http://my.test.proxy.example.com", diff --git a/seed/python-sdk/basic-auth-optional/poetry.lock b/seed/python-sdk/basic-auth-pw-omitted/poetry.lock similarity index 100% rename from seed/python-sdk/basic-auth-optional/poetry.lock rename to seed/python-sdk/basic-auth-pw-omitted/poetry.lock diff --git a/seed/python-sdk/basic-auth-optional/pyproject.toml b/seed/python-sdk/basic-auth-pw-omitted/pyproject.toml similarity index 94% rename from seed/python-sdk/basic-auth-optional/pyproject.toml rename to seed/python-sdk/basic-auth-pw-omitted/pyproject.toml index 18ff2a085e19..c7fdb906cd0c 100644 --- a/seed/python-sdk/basic-auth-optional/pyproject.toml +++ b/seed/python-sdk/basic-auth-pw-omitted/pyproject.toml @@ -1,9 +1,9 @@ [project] -name = "fern_basic-auth-optional" +name = "fern_basic-auth-pw-omitted" dynamic = ["version"] [tool.poetry] -name = "fern_basic-auth-optional" +name = "fern_basic-auth-pw-omitted" version = "0.0.1" description = "" readme = "README.md" @@ -38,7 +38,7 @@ packages = [ [tool.poetry.urls] Documentation = 'https://buildwithfern.com/learn' Homepage = 'https://buildwithfern.com/' -Repository = 'https://github.com/basic-auth-optional/fern' +Repository = 'https://github.com/basic-auth-pw-omitted/fern' [tool.poetry.dependencies] python = "^3.10" diff --git a/seed/python-sdk/basic-auth-optional/reference.md b/seed/python-sdk/basic-auth-pw-omitted/reference.md similarity index 90% rename from seed/python-sdk/basic-auth-optional/reference.md rename to seed/python-sdk/basic-auth-pw-omitted/reference.md index 6bda1fddd2f8..109a88868818 100644 --- a/seed/python-sdk/basic-auth-optional/reference.md +++ b/seed/python-sdk/basic-auth-pw-omitted/reference.md @@ -27,9 +27,9 @@ GET request with basic auth scheme
```python -from seed import SeedBasicAuthOptional +from seed import SeedBasicAuthPwOmitted -client = SeedBasicAuthOptional( +client = SeedBasicAuthPwOmitted( username="", password="", base_url="https://yourhost.com/path/to/api", @@ -90,9 +90,9 @@ POST request with basic auth scheme
```python -from seed import SeedBasicAuthOptional +from seed import SeedBasicAuthPwOmitted -client = SeedBasicAuthOptional( +client = SeedBasicAuthPwOmitted( username="", password="", base_url="https://yourhost.com/path/to/api", diff --git a/seed/python-sdk/basic-auth-optional/requirements.txt b/seed/python-sdk/basic-auth-pw-omitted/requirements.txt similarity index 100% rename from seed/python-sdk/basic-auth-optional/requirements.txt rename to seed/python-sdk/basic-auth-pw-omitted/requirements.txt diff --git a/seed/python-sdk/basic-auth-pw-omitted/snippet.json b/seed/python-sdk/basic-auth-pw-omitted/snippet.json new file mode 100644 index 000000000000..4287155ce181 --- /dev/null +++ b/seed/python-sdk/basic-auth-pw-omitted/snippet.json @@ -0,0 +1,31 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": "default", + "id": { + "path": "/basic-auth", + "method": "GET", + "identifier_override": "endpoint_basic-auth.getWithBasicAuth" + }, + "snippet": { + "sync_client": "from seed import SeedBasicAuthPwOmitted\n\nclient = SeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.basic_auth.get_with_basic_auth()\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedBasicAuthPwOmitted\n\nclient = AsyncSeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.basic_auth.get_with_basic_auth()\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/basic-auth", + "method": "POST", + "identifier_override": "endpoint_basic-auth.postWithBasicAuth" + }, + "snippet": { + "sync_client": "from seed import SeedBasicAuthPwOmitted\n\nclient = SeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.basic_auth.post_with_basic_auth(\n request={\"key\": \"value\"},\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedBasicAuthPwOmitted\n\nclient = AsyncSeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.basic_auth.post_with_basic_auth(\n request={\"key\": \"value\"},\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + } + ] +} \ No newline at end of file diff --git a/seed/python-sdk/basic-auth-optional/src/seed/__init__.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/__init__.py similarity index 86% rename from seed/python-sdk/basic-auth-optional/src/seed/__init__.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/__init__.py index a9c6abbc275e..8c924d9325c3 100644 --- a/seed/python-sdk/basic-auth-optional/src/seed/__init__.py +++ b/seed/python-sdk/basic-auth-pw-omitted/src/seed/__init__.py @@ -8,12 +8,12 @@ if typing.TYPE_CHECKING: from .errors import BadRequest, UnauthorizedRequest, UnauthorizedRequestErrorBody from . import basic_auth, errors - from .client import AsyncSeedBasicAuthOptional, SeedBasicAuthOptional + from .client import AsyncSeedBasicAuthPwOmitted, SeedBasicAuthPwOmitted from .version import __version__ _dynamic_imports: typing.Dict[str, str] = { - "AsyncSeedBasicAuthOptional": ".client", + "AsyncSeedBasicAuthPwOmitted": ".client", "BadRequest": ".errors", - "SeedBasicAuthOptional": ".client", + "SeedBasicAuthPwOmitted": ".client", "UnauthorizedRequest": ".errors", "UnauthorizedRequestErrorBody": ".errors", "__version__": ".version", @@ -44,9 +44,9 @@ def __dir__(): __all__ = [ - "AsyncSeedBasicAuthOptional", + "AsyncSeedBasicAuthPwOmitted", "BadRequest", - "SeedBasicAuthOptional", + "SeedBasicAuthPwOmitted", "UnauthorizedRequest", "UnauthorizedRequestErrorBody", "__version__", diff --git a/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/__init__.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/basic_auth/__init__.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/basic_auth/__init__.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/basic_auth/__init__.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/client.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/basic_auth/client.py similarity index 92% rename from seed/python-sdk/basic-auth-optional/src/seed/basic_auth/client.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/basic_auth/client.py index 2126381f8570..e9dfdd0fb5cb 100644 --- a/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/client.py +++ b/seed/python-sdk/basic-auth-pw-omitted/src/seed/basic_auth/client.py @@ -40,9 +40,9 @@ def get_with_basic_auth(self, *, request_options: typing.Optional[RequestOptions Examples -------- - from seed import SeedBasicAuthOptional + from seed import SeedBasicAuthPwOmitted - client = SeedBasicAuthOptional( + client = SeedBasicAuthPwOmitted( username="YOUR_USERNAME", password="YOUR_PASSWORD", base_url="https://yourhost.com/path/to/api", @@ -71,9 +71,9 @@ def post_with_basic_auth( Examples -------- - from seed import SeedBasicAuthOptional + from seed import SeedBasicAuthPwOmitted - client = SeedBasicAuthOptional( + client = SeedBasicAuthPwOmitted( username="YOUR_USERNAME", password="YOUR_PASSWORD", base_url="https://yourhost.com/path/to/api", @@ -118,9 +118,9 @@ async def get_with_basic_auth(self, *, request_options: typing.Optional[RequestO -------- import asyncio - from seed import AsyncSeedBasicAuthOptional + from seed import AsyncSeedBasicAuthPwOmitted - client = AsyncSeedBasicAuthOptional( + client = AsyncSeedBasicAuthPwOmitted( username="YOUR_USERNAME", password="YOUR_PASSWORD", base_url="https://yourhost.com/path/to/api", @@ -157,9 +157,9 @@ async def post_with_basic_auth( -------- import asyncio - from seed import AsyncSeedBasicAuthOptional + from seed import AsyncSeedBasicAuthPwOmitted - client = AsyncSeedBasicAuthOptional( + client = AsyncSeedBasicAuthPwOmitted( username="YOUR_USERNAME", password="YOUR_PASSWORD", base_url="https://yourhost.com/path/to/api", diff --git a/seed/python-sdk/basic-auth-optional/src/seed/basic_auth/raw_client.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/basic_auth/raw_client.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/basic_auth/raw_client.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/basic_auth/raw_client.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/client.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/client.py similarity index 96% rename from seed/python-sdk/basic-auth-optional/src/seed/client.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/client.py index ddc010416bd5..05e7158f26cd 100644 --- a/seed/python-sdk/basic-auth-optional/src/seed/client.py +++ b/seed/python-sdk/basic-auth-pw-omitted/src/seed/client.py @@ -12,7 +12,7 @@ from .basic_auth.client import AsyncBasicAuthClient, BasicAuthClient -class SeedBasicAuthOptional: +class SeedBasicAuthPwOmitted: """ Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. @@ -40,9 +40,9 @@ class SeedBasicAuthOptional: Examples -------- - from seed import SeedBasicAuthOptional + from seed import SeedBasicAuthPwOmitted - client = SeedBasicAuthOptional( + client = SeedBasicAuthPwOmitted( username="YOUR_USERNAME", password="YOUR_PASSWORD", base_url="https://yourhost.com/path/to/api", @@ -88,7 +88,7 @@ def basic_auth(self): return self._basic_auth -class AsyncSeedBasicAuthOptional: +class AsyncSeedBasicAuthPwOmitted: """ Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. @@ -116,9 +116,9 @@ class AsyncSeedBasicAuthOptional: Examples -------- - from seed import AsyncSeedBasicAuthOptional + from seed import AsyncSeedBasicAuthPwOmitted - client = AsyncSeedBasicAuthOptional( + client = AsyncSeedBasicAuthPwOmitted( username="YOUR_USERNAME", password="YOUR_PASSWORD", base_url="https://yourhost.com/path/to/api", diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/__init__.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/__init__.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/__init__.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/__init__.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/api_error.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/api_error.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/api_error.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/api_error.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/client_wrapper.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/client_wrapper.py similarity index 97% rename from seed/python-sdk/basic-auth-optional/src/seed/core/client_wrapper.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/client_wrapper.py index d364f6d1f7bd..fb36028c9dc9 100644 --- a/seed/python-sdk/basic-auth-optional/src/seed/core/client_wrapper.py +++ b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/client_wrapper.py @@ -29,11 +29,11 @@ def get_headers(self) -> typing.Dict[str, str]: import platform headers: typing.Dict[str, str] = { - "User-Agent": "fern_basic-auth-optional/0.0.1", + "User-Agent": "fern_basic-auth-pw-omitted/0.0.1", "X-Fern-Language": "Python", "X-Fern-Runtime": f"python/{platform.python_version()}", "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", - "X-Fern-SDK-Name": "fern_basic-auth-optional", + "X-Fern-SDK-Name": "fern_basic-auth-pw-omitted", "X-Fern-SDK-Version": "0.0.1", **(self.get_custom_headers() or {}), } diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/datetime_utils.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/datetime_utils.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/datetime_utils.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/datetime_utils.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/file.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/file.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/file.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/file.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/force_multipart.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/force_multipart.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/force_multipart.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/force_multipart.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_client.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_client.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/http_client.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_client.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_response.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_response.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/http_response.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_response.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/__init__.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_sse/__init__.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/__init__.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_sse/__init__.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_api.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_sse/_api.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_api.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_sse/_api.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_decoders.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_decoders.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_exceptions.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_exceptions.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_models.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/http_sse/_models.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/jsonable_encoder.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/jsonable_encoder.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/jsonable_encoder.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/jsonable_encoder.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/logging.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/logging.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/logging.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/logging.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/parse_error.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/parse_error.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/parse_error.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/parse_error.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/pydantic_utilities.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/pydantic_utilities.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/pydantic_utilities.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/pydantic_utilities.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/query_encoder.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/query_encoder.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/query_encoder.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/query_encoder.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/remove_none_from_dict.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/remove_none_from_dict.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/remove_none_from_dict.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/remove_none_from_dict.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/request_options.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/request_options.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/request_options.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/request_options.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/core/serialization.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/core/serialization.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/core/serialization.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/core/serialization.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/__init__.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/__init__.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/errors/__init__.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/__init__.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/__init__.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/errors/__init__.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/errors/errors/__init__.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/errors/__init__.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/bad_request.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/errors/bad_request.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/errors/errors/bad_request.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/errors/bad_request.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/errors/unauthorized_request.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/errors/unauthorized_request.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/errors/errors/unauthorized_request.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/errors/unauthorized_request.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/types/__init__.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/types/__init__.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/errors/types/__init__.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/types/__init__.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/errors/types/unauthorized_request_error_body.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/types/unauthorized_request_error_body.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/errors/types/unauthorized_request_error_body.py rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/errors/types/unauthorized_request_error_body.py diff --git a/seed/python-sdk/basic-auth-optional/src/seed/py.typed b/seed/python-sdk/basic-auth-pw-omitted/src/seed/py.typed similarity index 100% rename from seed/python-sdk/basic-auth-optional/src/seed/py.typed rename to seed/python-sdk/basic-auth-pw-omitted/src/seed/py.typed diff --git a/seed/python-sdk/basic-auth-pw-omitted/src/seed/version.py b/seed/python-sdk/basic-auth-pw-omitted/src/seed/version.py new file mode 100644 index 000000000000..d14dbfe2d90b --- /dev/null +++ b/seed/python-sdk/basic-auth-pw-omitted/src/seed/version.py @@ -0,0 +1,3 @@ +from importlib import metadata + +__version__ = metadata.version("fern_basic-auth-pw-omitted") diff --git a/seed/python-sdk/basic-auth-optional/tests/custom/test_client.py b/seed/python-sdk/basic-auth-pw-omitted/tests/custom/test_client.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/custom/test_client.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/custom/test_client.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/__init__.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/__init__.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/__init__.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/__init__.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/__init__.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/__init__.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/assets/models/__init__.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/__init__.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/circle.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/circle.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/assets/models/circle.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/circle.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/color.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/color.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/assets/models/color.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/color.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_defaults.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/object_with_defaults.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_defaults.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/object_with_defaults.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_optional_field.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/object_with_optional_field.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/assets/models/object_with_optional_field.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/object_with_optional_field.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/shape.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/shape.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/assets/models/shape.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/shape.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/square.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/square.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/assets/models/square.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/square.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/assets/models/undiscriminated_shape.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/undiscriminated_shape.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/assets/models/undiscriminated_shape.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/assets/models/undiscriminated_shape.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/test_http_client.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/test_http_client.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/test_http_client.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/test_http_client.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/test_query_encoding.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/test_query_encoding.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/test_query_encoding.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/test_query_encoding.py diff --git a/seed/python-sdk/basic-auth-optional/tests/utils/test_serialization.py b/seed/python-sdk/basic-auth-pw-omitted/tests/utils/test_serialization.py similarity index 100% rename from seed/python-sdk/basic-auth-optional/tests/utils/test_serialization.py rename to seed/python-sdk/basic-auth-pw-omitted/tests/utils/test_serialization.py diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/api.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/api.yml similarity index 86% rename from test-definitions/fern/apis/basic-auth-optional/definition/api.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/definition/api.yml index 8b1d72b0b769..db01794de599 100644 --- a/test-definitions/fern/apis/basic-auth-optional/definition/api.yml +++ b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/api.yml @@ -1,4 +1,4 @@ -name: basic-auth-optional +name: basic-auth-pw-omitted auth: Basic auth-schemes: Basic: diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/basic-auth.yml similarity index 100% rename from test-definitions/fern/apis/basic-auth-optional/definition/basic-auth.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/definition/basic-auth.yml diff --git a/test-definitions/fern/apis/basic-auth-optional/definition/errors.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/definition/errors.yml similarity index 100% rename from test-definitions/fern/apis/basic-auth-optional/definition/errors.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/definition/errors.yml diff --git a/test-definitions/fern/apis/basic-auth-optional/generators.yml b/test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml similarity index 86% rename from test-definitions/fern/apis/basic-auth-optional/generators.yml rename to test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml index b30d6ed97cd2..fc35e4407493 100644 --- a/test-definitions/fern/apis/basic-auth-optional/generators.yml +++ b/test-definitions/fern/apis/basic-auth-pw-omitted/generators.yml @@ -9,7 +9,7 @@ groups: token: ${GITHUB_TOKEN} mode: push uri: fern-api/php-sdk-tests - branch: basic-auth-optional + branch: basic-auth-pw-omitted go-sdk: generators: - name: fernapi/fern-go-sdk @@ -19,4 +19,4 @@ groups: token: ${GITHUB_TOKEN} mode: push uri: fern-api/go-sdk-tests - branch: basic-auth-optional + branch: basic-auth-pw-omitted From b85372b094cb3db0244de45e27c8fc070439d088 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:02:14 +0000 Subject: [PATCH 10/15] fix(python-sdk): bump version to 5.4.0 (feat requires minor bump) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/python/sdk/versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators/python/sdk/versions.yml b/generators/python/sdk/versions.yml index 4654e1aee08c..9a4f677b11b9 100644 --- a/generators/python/sdk/versions.yml +++ b/generators/python/sdk/versions.yml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json # For unreleased changes, use unreleased.yml -- version: 5.2.2 +- version: 5.4.0 changelogEntry: - summary: | Support omitting username or password from basic auth when configured via From dd520adc88225ebe05739b96cb53b1999f76a5c1 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:25:04 +0000 Subject: [PATCH 11/15] fix(python-sdk): correct createdAt date to 2026-04-03 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- generators/python/sdk/versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators/python/sdk/versions.yml b/generators/python/sdk/versions.yml index ebef531ad913..f289dd409d37 100644 --- a/generators/python/sdk/versions.yml +++ b/generators/python/sdk/versions.yml @@ -9,7 +9,7 @@ password encodes `username:`, omitting username encodes `:password`). When both are omitted, the Authorization header is skipped entirely. type: feat - createdAt: "2026-03-31" + createdAt: "2026-04-03" irVersion: 65 - version: 5.3.1 From b2e22fd33237ffd2d335332610cb13f1da85585e Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:51:00 +0000 Subject: [PATCH 12/15] fix(python-sdk): handle usernameOmit/passwordOmit in dynamic snippets generator Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/EndpointSnippetGenerator.ts | 20 +++++++++++++------ .../basic-auth-pw-omitted/snippet.json | 10 +++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/generators/python-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/python-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index c6a08dee8fbc..a910c180439a 100644 --- a/generators/python-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/python-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -293,16 +293,24 @@ export class EndpointSnippetGenerator { auth: FernIr.dynamic.BasicAuth; values: FernIr.dynamic.BasicAuthValues; }): python.NamedValue[] { - return [ - { + // usernameOmit/passwordOmit may exist in newer IR versions + const authRecord = auth as unknown as Record; + const usernameOmitted = authRecord.usernameOmit === true; + const passwordOmitted = authRecord.passwordOmit === true; + const args: python.NamedValue[] = []; + if (!usernameOmitted) { + args.push({ name: this.context.getPropertyName(auth.username), value: python.TypeInstantiation.str(values.username) - }, - { + }); + } + if (!passwordOmitted) { + args.push({ name: this.context.getPropertyName(auth.password), value: python.TypeInstantiation.str(values.password) - } - ]; + }); + } + return args; } private getConstructorBearerAuthArgs({ diff --git a/seed/python-sdk/basic-auth-pw-omitted/snippet.json b/seed/python-sdk/basic-auth-pw-omitted/snippet.json index 4287155ce181..0d3ccd66b2e0 100644 --- a/seed/python-sdk/basic-auth-pw-omitted/snippet.json +++ b/seed/python-sdk/basic-auth-pw-omitted/snippet.json @@ -9,8 +9,8 @@ "identifier_override": "endpoint_basic-auth.getWithBasicAuth" }, "snippet": { - "sync_client": "from seed import SeedBasicAuthPwOmitted\n\nclient = SeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.basic_auth.get_with_basic_auth()\n", - "async_client": "import asyncio\n\nfrom seed import AsyncSeedBasicAuthPwOmitted\n\nclient = AsyncSeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.basic_auth.get_with_basic_auth()\n\n\nasyncio.run(main())\n", + "sync_client": "from seed import SeedBasicAuthPwOmitted\n\nclient = SeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.basic_auth.get_with_basic_auth()\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedBasicAuthPwOmitted\n\nclient = AsyncSeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.basic_auth.get_with_basic_auth()\n\n\nasyncio.run(main())\n", "type": "python" } }, @@ -22,10 +22,10 @@ "identifier_override": "endpoint_basic-auth.postWithBasicAuth" }, "snippet": { - "sync_client": "from seed import SeedBasicAuthPwOmitted\n\nclient = SeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.basic_auth.post_with_basic_auth(\n request={\"key\": \"value\"},\n)\n", - "async_client": "import asyncio\n\nfrom seed import AsyncSeedBasicAuthPwOmitted\n\nclient = AsyncSeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n password=\"YOUR_PASSWORD\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.basic_auth.post_with_basic_auth(\n request={\"key\": \"value\"},\n )\n\n\nasyncio.run(main())\n", + "sync_client": "from seed import SeedBasicAuthPwOmitted\n\nclient = SeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.basic_auth.post_with_basic_auth(\n request={\"key\": \"value\"},\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedBasicAuthPwOmitted\n\nclient = AsyncSeedBasicAuthPwOmitted(\n username=\"YOUR_USERNAME\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.basic_auth.post_with_basic_auth(\n request={\"key\": \"value\"},\n )\n\n\nasyncio.run(main())\n", "type": "python" } } ] -} \ No newline at end of file +} From 406c776e497bf6a065f78b7da06e2a93c73db3be Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:13:25 +0000 Subject: [PATCH 13/15] refactor(python-sdk): simplify omit checks from === true to !! Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../dynamic-snippets/src/EndpointSnippetGenerator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generators/python-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts b/generators/python-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts index a910c180439a..5e35b2d0bcce 100644 --- a/generators/python-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts +++ b/generators/python-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts @@ -295,8 +295,8 @@ export class EndpointSnippetGenerator { }): python.NamedValue[] { // usernameOmit/passwordOmit may exist in newer IR versions const authRecord = auth as unknown as Record; - const usernameOmitted = authRecord.usernameOmit === true; - const passwordOmitted = authRecord.passwordOmit === true; + const usernameOmitted = !!authRecord.usernameOmit; + const passwordOmitted = !!authRecord.passwordOmit; const args: python.NamedValue[] = []; if (!usernameOmitted) { args.push({ From 96607eef8958c42d69c570092ecbdaa25a235b66 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:54:07 +0000 Subject: [PATCH 14/15] fix: pass usernameOmit/passwordOmit through DynamicSnippetsConverter to dynamic IR The DynamicSnippetsConverter was constructing dynamic BasicAuth with only username and password fields, dropping usernameOmit/passwordOmit from the main IR's BasicAuthScheme. This caused dynamic snippets generators to always include omitted auth fields (e.g. $password) since they couldn't detect the omit flags in the dynamic IR data. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../DynamicSnippetsConverter.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts b/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts index b1750ea01c28..5675bc14999b 100644 --- a/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts +++ b/packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts @@ -732,11 +732,22 @@ export class DynamicSnippetsConverter { } const scheme = auth.schemes[0]; switch (scheme.type) { - case "basic": - return DynamicSnippets.Auth.basic({ + case "basic": { + const basicAuth: DynamicSnippets.BasicAuth & { + usernameOmit?: boolean; + passwordOmit?: boolean; + } = { username: this.inflateName(scheme.username), password: this.inflateName(scheme.password) - }); + }; + if (scheme.usernameOmit) { + basicAuth.usernameOmit = scheme.usernameOmit; + } + if (scheme.passwordOmit) { + basicAuth.passwordOmit = scheme.passwordOmit; + } + return DynamicSnippets.Auth.basic(basicAuth); + } case "bearer": return DynamicSnippets.Auth.bearer({ token: this.inflateName(scheme.token) From ad58c5a5743563d659a602f49ab592df6e20c1bb Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Sat, 4 Apr 2026 03:10:02 +0000 Subject: [PATCH 15/15] ci: retrigger CI (flaky test-ete timeout) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>