diff --git a/.github/renovate.json b/.github/renovate.json index d0cc4a064..a41a99a8f 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,10 +1,12 @@ { "extends": [ - "config:base", - ":semanticCommitTypeAll(chore)" + "config:recommended", + ":semanticCommitTypeAll(chore)", + "helpers:pinGitHubActionDigestsToSemver" ], "rangeStrategy": "widen", "lockFileMaintenance": { "enabled": true, "automerge": true }, + "minimumReleaseAge": "7 days", "regexManagers": [ { "fileMatch": [ diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 0a1cf6055..7b8277ad5 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -11,13 +11,13 @@ jobs: test: strategy: matrix: - python: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + python: [ "3.11", "3.12", "3.13", "3.14", "3.15" ] os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: ${{ matrix.python }} @@ -27,7 +27,7 @@ jobs: shell: bash - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .venv key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-dependencies-${{ hashFiles('**/pdm.lock') }} @@ -37,7 +37,7 @@ jobs: run: pip install pdm - name: Install Dependencies - run: pdm install + run: pdm sync --clean - name: Check formatting run: pdm run ruff format . --check @@ -60,7 +60,7 @@ jobs: if: matrix.os == 'ubuntu-latest' - name: Store coverage report - uses: actions/upload-artifact@v4.5.0 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: matrix.os == 'ubuntu-latest' with: name: coverage-${{ matrix.python }} @@ -74,11 +74,11 @@ jobs: os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: - python-version: "3.9" + python-version: "3.11" - name: Get Python Version id: get_python_version @@ -86,7 +86,7 @@ jobs: shell: bash - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .venv key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-min-dependencies-${{ hashFiles('**/pdm.lock') }} @@ -96,14 +96,11 @@ jobs: run: pip install pdm - name: Install minimum dependencies - run: pdm install -L pdm.minimal.lock + run: pdm sync --clean -L pdm.minimal.lock - name: Run mypy run: pdm mypy --show-error-codes - - name: Lint - run: pdm run ruff check . - - name: Run unit tests only # snapshots are expected to fail run: pdm unit_test @@ -112,12 +109,12 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.2.2 - - uses: actions/setup-python@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: "3.12" - name: Download coverage reports - uses: actions/download-artifact@v4.1.8 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: merge-multiple: true @@ -131,18 +128,18 @@ jobs: # Find all of the downloaded coverage reports and combine them .venv/bin/python -m coverage combine - + # Create html report .venv/bin/python -m coverage html --skip-covered --skip-empty - + # Report in Markdown and write to summary. .venv/bin/python -m coverage report --format=markdown >> $GITHUB_STEP_SUMMARY - + # Report again and fail if under 100%. - .venv/bin/python -m coverage report --fail-under=100 + .venv/bin/python -m coverage report --fail-under=100 - name: Upload HTML report if check failed. - uses: actions/upload-artifact@v4.5.0 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: html-report path: htmlcov @@ -153,51 +150,35 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - httpx_version: - - "0.20.0" - - "" + lockfile: + - "pdm.lock" + - "pdm.minimal.lock" services: openapi-test-server: - image: ghcr.io/openapi-generators/openapi-test-server:0.0.1 + image: ghcr.io/openapi-generators/openapi-test-server:0.2.1 ports: - "3000:3000" steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: - python-version: "3.9" + python-version: "3.11" - name: Get Python Version id: get_python_version run: echo "python_version=$(python --version)" >> $GITHUB_OUTPUT - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: .venv - key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-dependencies-${{ hashFiles('**/pdm.lock') }} - restore-keys: | - ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-dependencies - - name: Install dependencies - run: | - pip install pdm - python -m venv .venv - pdm install - name: Cache Generated Client Dependencies - uses: actions/cache@v4 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: integration-tests/.venv - key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-integration-dependencies-${{ hashFiles('**/pdm.lock') }} + key: ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-integration-dependencies-${{ hashFiles('integration-tests/pdm*.lock') }} restore-keys: | ${{ runner.os }}-${{ steps.get_python_version.outputs.python_version }}-integration-dependencies - - name: Set httpx version - if: matrix.httpx_version != '' - run: | - cd integration-tests - pdm add httpx==${{ matrix.httpx_version }} - name: Install Integration Dependencies run: | cd integration-tests - pdm install + pip install pdm + pdm sync --clean -L ${{ matrix.lockfile }} - name: Run Tests run: | cd integration-tests diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 000000000..fa4511eff --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,33 @@ +name: Fuzz code generation + +permissions: + contents: read + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: "23 8 * * *" + workflow_dispatch: + +jobs: + fuzz: + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + OPENAPI_PYTHON_CLIENT_FUZZ_EXAMPLES: ${{ github.event_name == 'schedule' && '1000' || '100' }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.14" + cache: pip + - name: Install PDM + run: pip install pdm + - name: Install dependencies + run: pdm sync --clean + - name: Fuzz generated Python + run: pdm fuzz diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78c3e09da..fff6bd455 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,12 +12,12 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Hatchling run: pip install --upgrade hatchling - name: Build run: hatchling build - name: Push to PyPI - uses: pypa/gh-action-pypi-publish@v1.12.3 + uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3 with: attestations: true diff --git a/.gitignore b/.gitignore index b04b9f514..f7fcfaff6 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,5 @@ test-reports/ htmlcov/ # Generated end to end test data -my-test-api-client/ -custom-e2e/ -3-1-features-client \ No newline at end of file +tests/tmp +end_to_end_tests/tmp/* \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6569f30fb..10deff59d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,408 @@ Programmatic usage of this project (e.g., importing it as a Python module) and t The 0.x prefix used in versions for this project is to indicate that breaking changes are expected frequently (several times a year). Breaking changes will increment the minor number, all other changes will increment the patch number. You can track the progress toward 1.0 [here](https://github.com/openapi-generators/openapi-python-client/projects/2). +## 0.30.0 (2026-11-07) + +### ❗️Breaking ❗ + +#### Switch from `httpx` to `httpx2` in generated clients + +[`httpx2`](https://pydantic.dev/docs/httpx2/get-started/) is largely compatible with `httpx` and should only be a breaking change for users who customize the internal `Client`. + +Thanks to @s-sajid-ali and @johnthagen for making this one happen in PR #1461 and discussion #1455! + +### 🚀 Features + +- Update `uv_build` to 0.13 when using `--meta=uv` (#1507) + +### 🐛 Fixes + +- Let httpx2 choose the multipart/form-data boundary (#1511) + +#### Let httpx2 choose the boundary for `multipart/form-data` requests + +Since 0.29.0, generated endpoints sent every `multipart/form-data` body with the fixed boundary `+++`, so an uploaded file containing `\r\n--+++` was cut short at that point (#1509). These bodies no longer set `Content-Type`, so httpx2 sets it with a random boundary for each request, as single-body endpoints did before 0.29.0. Endpoints with several body types still get a boundary (#947). + +## 0.29.1 (2026-08-30) + +### 🚨Security + +#### Arbitrary code generation vulnerability + +Prior to this release, malicious OpenAPI documents could cause openapi-python-client to generate arbitrary code, which +would then be executed by consumers of the generated client. + +If you generate code from OpenAPI documents you don't control, you should upgrade to this release as **soon as possible** +and validate any previously-generated code. + +See [the GitHub advisory](https://github.com/openapi-generators/openapi-python-client/security/advisories/GHSA-5293-mq8x-g3xj) for more details. + +### 🚀 Features + +- Update `uv_build` to 0.12 when using `--meta=uv` (#1473) + +### 🐛 Fixes + +- Apply PEP 639 for improved license metadata (#1459) +- update generated code to use `StrEnum` and `-> Self` (#1474) +- Remove trailing spaces in README example code (#1475) +- Stopped generating empty docstrings for models with no description + +#### Fixed invalid Python identifiers when resolving naming conflicts + +When two property or parameter names conflicted after conversion to `snake_case` (e.g. `foo-bar` and `fooBar`), the conflict-resolution path preserved delimiters like `-`, `.`, and spaces in the generated Python identifiers, producing invalid code which failed generation. Conflicting names now keep their original casing but have any characters which are invalid in Python identifiers stripped (e.g. `foobar` and `fooBar`). + +#### Improve readability of error messages + +Errors and warnings which include a snippet of your OpenAPI document now render that snippet as JSON, making them much easier to read. + +### 📝Notes + +#### Breaking changes for all custom templates + +**ALL** custom templates are expected to break with this version as a result of the security fix. + +1. The `utils` global has been renamed to `strings` +2. Most string values can no longer be rendered directly into templates, + you must describe how the value is being used so it can be properly escaped using either a Python function or Jinja filter: + 1. `strings.snake_case()` / `| snakecase` (existing) + 2. `strings.kebab_case()` / `| kebabcase` (existing) + 3. `strings.pascal_case()` / `| pascalcase` (existing) + 4. `python_identifier()` (existing) + 5. `class_name()` (existing) + 6. `strings.safe_for_docstring()` / `| safe_for_docstring` for values which get injected into a `"""` docstring + 7. `strings.in_f_string_literal()` / `| in_f_string_literal` for values that go into `f""` f-strings + 8. `strings.in_double_quote_literal()` / `| in_double_quote_literal` for values that go into **non-f-string** `""` literals + 9. `.as_unembedded_code()` / `| as_unembedded_code` ONLY for `PythonCode` values—those that are intended to be Python code which is not embedded into any string/docstring. Examples include usages of `.python_code`, `.get_type_string()`, `.get_instance_type_string()`, `.get_type_strings_in_union()`. You *should not* assume these values are safe to put in docstrings, string literals, or f-string literals. Use the dedicated helpers for those. +As always, you can check the diff of the built in templates for examples. You will also want to check generated output +for "UntrustedString", which is how any string now requiring one of those functions will appear. + +#### Many control characters now stripped from string literals + +Out of an abundance of caution, most Unicode control characters are now stripped from string literals. +If your API uses control characters as part of const values, enums, or JSON body property names you may have issues +with this new version. + +Most APIs are not expected to be affected by this change. + +## 0.29.0 (2026-05-30) + +### Breaking Changes + +- Drop support for Python 3.10 +- Raise minimum httpx version to 0.23.1 + +### Features + +- replace python-dateutil with stdlib datetime.fromisoformat (#1429) + +### Fixes + +- Remove some generated casts that aren't necessary with mypy v2 (#1436) +- Explicitly set boundary for multipart/form-data (#1005) + +## 0.28.4 (2026-05-11) + +### Features + +- Update `uv_build` to 0.11 when using `--meta=uv` (#1434) + +#### Add support for x-enum-varnames to string enums + +##1358 by @mbbush + +You can now customize the variable names of the generated string enumerations using the x-enum-varnames openapi extension. Previously, this was only possible for integer enumerations. + +## 0.28.3 (2026-02-17) + +### Fixes + +- sort remaining lazy imports in model template (#1400) + +## 0.28.2 (2026-02-09) + +### Features + +- Update `uv_build` 0.10 when using `--meta=uv` (#1396) + +## 0.28.1 (2026-01-10) + +### Fixes + +- Apply required overrides from allOf schemas (#1384) +- Sort lazy imports to increase stability of generated code (#1378) + +## 0.28.0 (2025-12-03) + +### Breaking Changes + +- URL-encode path parameters in generated endpoints (#1349) + +### Fixes + +#### Fix bad code generation + +##1360 by @EricAtORS + +This fixes: +- missing parenthesis in to_multipart + #1338 #1318 +- missing imports in the lazy eval in to_multipart: +##931 and #1051 + +#### Fix optional bodies + +If a body is not required (the default), it will now: + +1. Have `Unset` as part of its type annotation. +2. Default to a value of `UNSET` +3. Not be included in the request if it is `UNSET` + +Thanks @orelmaliach for the report! Fixes #1354 + +## 0.27.1 (2025-11-03) + +### Fixes + +- Remove non-existent CHANGELOG.md references from UV and Poetry templates (#1344) +- Initialize optional lists as UNSET, not [] (#1346) +- Correct docstring typos in client template (#1347) +- Replace bare except blocks with specific exception types (#1348) + +#### Update `uv_build` to 0.9 + +##1352 by @johnthagen + +`uv` has been in the `0.9.x` release cycle for a while, so update templates to use the corresponding `uv_build` range. + +## 0.27.0 (2025-10-28) + +### Breaking Changes + +#### Drop support for Python 3.9 + +Both `openapi-python-client` itself and any generated clients no longer support Python 3.9. + +#### Generated models now use `from __future__ import annotations` + +This simplifies using forward references with the newer union syntax. + +### Features + +#### Upgrade generated clients to 3.10 union syntax + +All generated types now use the `A | B` syntax instead of `Union[A, B]` or `Optional[A]`. + +### Fixes + +- Drop generated `requires-python` upper bounds for uv and PDM (#1329) + +#### Change default Ruff hook to `--fix-only` + +This should enable `openapi-python-client` to keep auto-fixing lints (like removing unused imports) but _not_ fail to +generate when unfixable lints are violated. + +Since it's now unlikely for breaking changes to affect our usage (and by popular request), the upper bound of `ruff` +has been lifted. Newer versions of `openapi-python-client` should no longer be required to support newer versions of `ruff`. + +### Notes + +- Minimum Typer version is now 0.16 + +## 0.26.2 (2025-10-06) + +### Fixes + +- ambigious tilde specifier requires-python with`--meta=uv` (#1321) + +## 0.26.1 (2025-09-13) + +### Features + +- Reference schema support (#800) (#1307) +- Support Ruff 0.13 + +## 0.26.0 (2025-08-26) + +### Breaking Changes + +#### Change some union variant names + +When creating a union with `oneOf`, `anyOf`, or a list of `type`, the name of each variant used to be `type_{index}` +where the index is based on the order of the types in the union. + +This made some modules difficult to understand, what is a `my_type_type_0` after all? +It also meant that reordering union members, while not a breaking change to the API, _would_ be a breaking change +for generated clients. + +Now, if an individual variant has a `title` attribute, that `title` will be used in the name instead. +This is only an enhancement for documents which use `title` in union variants, and only a breaking change for +_inline models_ (not `#/components/schemas` which should already have used more descriptive names). + +Thanks @wallagib for PR #962! + +### Features + +#### Support patterned and default HTTP statuses + +HTTP statuses like `2XX` and `default` are now supported! + +A big thank you to: +- @PSU3D0 for PR #973 (eons ago 😅) +- @obs-gh-peterkolloch for PR #1300 +- @goodsonjr for PR #1304 + +Closes #1271 and #832 + +> [!NOTE] +> Custom template users: the `endpoint.responses` type has changed quite a bit. Check out #1303 for the changes. + +## 0.25.3 (2025-07-21) + +### Features + +- Add --meta uv for generating astral-sh/uv compatible packages. (#1286) +- Switch to `uv_build` build backend. (#1290) + +## 0.25.2 (2025-07-03) + +### Fixes + +- Import error for `types.FileType` (#1274) (#1278) + +## 0.25.1 (2025-06-19) + +### Fixes + +- Support ruff 0.12 (#1270) + +## 0.25.0 (2025-06-06) + +### Breaking Changes + +- Raise minimum httpx version to 0.23 + +#### Removed ability to set an array as a multipart body + +Previously, when defining a request's body as `multipart/form-data`, the generator would attempt to generate code +for both `object` schemas and `array` schemas. However, most arrays could not generate valid multipart bodies, as +there would be no field names (required to set the `Content-Disposition` headers). + +The code to generate any body for `multipart/form-data` where the schema is `array` has been removed, and any such +bodies will be skipped. This is not _expected_ to be a breaking change in practice, since the code generated would +probably never work. + +If you have a use-case for `multipart/form-data` with an `array` schema, please [open a new discussion](https://github.com/openapi-generators/openapi-python-client/discussions) with an example schema and the desired functional Python code. + +#### Change default multipart array serialization + +Previously, any arrays of values in a `multipart/form-data` body would be serialized as an `application/json` part. +This matches the default behavior specified by OpenAPI and supports arrays of files (`binary` format strings). +However, because this generator doesn't yet support specifying `encoding` per property, this may result in +now-incorrect code when the encoding _was_ explicitly set to `application/json` for arrays of scalar values. + +PR #938 fixes #692. Thanks @micha91 for the fix, @ratgen and @FabianSchurig for testing, and @davidlizeng for the original report... many years ago 😅. + +## 0.24.3 (2025-03-31) + +### Features + +#### Adding support for named integer enums + +##1214 by @barrybarrette + +Adding support for named integer enums via an optional extension, `x-enum-varnames`. + +This extension is added to the schema inline with the `enum` definition: +``` +"MyEnum": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer", + "format": "int32", + "x-enum-varnames": [ + "Deinstalled", + "Installed", + "Upcoming_Site", + "Lab_Site", + "Pending_Deinstall", + "Suspended", + "Install_In_Progress", + "Unknown" + ] +} +``` + +The result: +![image](https://github.com/user-attachments/assets/780880b3-2f1f-49be-823b-f9abb713a3e1) + +## 0.24.2 (2025-03-22) + +### Fixes + +#### Make lists of models and enums work correctly in custom templates + +Lists of model and enum classes should be available to custom templates via the Jinja +variables `openapi.models` and `openapi.enums`, but these were being passed in a way that made +them always appear empty. This has been fixed so a custom template can now iterate over them. + +Closes #1188. + +## 0.24.1 (2025-03-15) + +### Features + +- allow Ruff to 0.10 (#1220) +- allow Ruff 0.11 (#1222) +- Allow any `Mapping` in generated `from_dict` functions (#1211) + +### Fixes + +#### Always parse `$ref` as a reference + +If additional attributes were included with a `$ref` (for example `title` or `description`), the property could be +interpreted as a new type instead of a reference, usually resulting in `Any` in the generated code. +Now, any sibling properties to `$ref` will properly be ignored, as per the OpenAPI specification. + +Thanks @nkrishnaswami! + +## 0.24.0 (2025-03-03) + +### Breaking Changes + +#### Support `$ref` in responses + +Previously, using a `$ref` to define a response was ignored, the code to call the endpoint was still generated, but +the response would not be parsed. Now, responses defined with `$ref` will be used to generate the response model, which +will parse the response at runtime. + +If a `$ref` is incorrect or uses a feature that is not supported by the generator, these endpoints will start failing to +generate. + +### Features + +#### Make `config` available in custom templates + +The configuration options object is now exposed as a variable called `config` in Jinja2 templates. + +#### Add `docstrings_on_attributes` config setting + +Setting this option to `true` changes the docstring behavior in model classes: for any attribute that have a non-empty `description`, instead of describing the attribute as part of the class's docstring, the description will appear in an individual docstring for that attribute. + +## 0.23.1 (2025-01-13) + +### Features + +- allow Ruff 0.9 (#1192) + ## 0.23.0 (2024-12-24) ### Breaking Changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 194f26dcc..e3d9c68ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,26 +50,40 @@ All changes must be tested, I recommend writing the test first, then writing the If you think that some of the added code is not testable (or testing it would add little value), mention that in your PR and we can discuss it. -1. If you're adding support for a new OpenAPI feature or covering a new edge case, add an [end-to-end test](#end-to-end-tests) -2. If you're modifying the way an existing feature works, make sure an existing test generates the _old_ code in `end_to_end_tests/golden-record`. You'll use this to check for the new code once your changes are complete. -3. If you're improving an error or adding a new error, add a [unit test](#unit-tests) +1. If you're adding support for a new OpenAPI feature or covering a new edge case, add [functional tests](#functional-tests), and optionally an [end-to-end snapshot test](#end-to-end-snapshot-tests). +2. If you're modifying the way an existing feature works, make sure functional tests cover this case. Existing end-to-end snapshot tests might also be affected if you have changed what generated model/endpoint code looks like. +3. If you're improving error handling or adding a new error, add [functional tests](#functional-tests). +4. For tests of low-level pieces of code that are fairly self-contained, and not tightly coupled to other internal implementation details, you can use regular [unit tests](#unit-tests). -#### End-to-end tests +#### End-to-end snapshot tests -This project aims to have all "happy paths" (types of code which _can_ be generated) covered by end to end tests (snapshot tests). In order to check code changes against the previous set of snapshots (called a "golden record" here), you can run `pdm e2e`. To regenerate the snapshots, run `pdm regen`. +This project aims to have all "happy paths" (types of code which _can_ be generated) covered by end-to-end tests. There are two types of these: snapshot tests, and functional tests. -There are 4 types of snapshots generated right now, you may have to update only some or all of these depending on the changes you're making. Within the `end_to_end_tets` directory: +Snapshot tests verify that the generated code is identical to a previously-committed set of snapshots (called a "golden record" here). They are basically regression tests to catch any unintended changes in the generator output. + +In order to check code changes against the previous set of snapshots (called a "golden record" here), you can run `pdm e2e`. To regenerate the snapshots, run `pdm regen`. + +There are 4 types of snapshots generated right now, you may have to update only some or all of these depending on the changes you're making. Within the `end_to_end_tests` directory: 1. `baseline_openapi_3.0.json` creates `golden-record` for testing OpenAPI 3.0 features 2. `baseline_openapi_3.1.yaml` is checked against `golden-record` for testing OpenAPI 3.1 features (and ensuring consistency with 3.0) 3. `test_custom_templates` are used with `baseline_openapi_3.0.json` to generate `custom-templates-golden-record` for testing custom templates 4. `3.1_specific.openapi.yaml` is used to generate `test-3-1-golden-record` and test 3.1-specific features (things which do not have a 3.0 equivalent) +#### Functional tests + +These are black-box tests that verify the runtime behavior of generated code, as well as the generator's validation behavior. They are also end-to-end tests, since they run the generator as a shell command. + +This can sometimes identify issues with error handling, validation logic, module imports, etc., that might be harder to diagnose via the snapshot tests, especially during development of a new feature. For instance, they can verify that JSON data is correctly decoded into model class attributes, or that the generator will emit an appropriate warning or error for an invalid spec. + +See [`end_to_end_tests/functional_tests`](./end_to_end_tests/functional_tests). + #### Unit tests -> **NOTE**: Several older-style unit tests using mocks exist in this project. These should be phased out rather than updated, as the tests are brittle and difficult to maintain. Only error cases should be tests with unit tests going forward. +These include: -In some cases, we need to test things which cannot be generated—like validating that errors are caught and handled correctly. These should be tested via unit tests in the `tests` directory, using the `pytest` framework. +* Regular unit tests of basic pieces of fairly self-contained low-level functionality, such as helper functions. These are implemented in the `tests` directory, using the `pytest` framework. +* Older-style unit tests of low-level functions like `property_from_data` that have complex behavior. These are brittle and difficult to maintain, and should not be used going forward. Instead, they should be migrated to functional tests. ### Creating a Pull Request diff --git a/README.md b/README.md index a184be377..bdaf6bfce 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ _Be forewarned, this is a beta-level feature in the sense that the API exposed i ## What You Get -1. A `pyproject.toml` file, optionally with [Poetry] metadata (default), [PDM] (with `--meta=pdm`), or only [Ruff] config. +1. A `pyproject.toml` file, optionally with [Poetry] metadata (default), [uv] (with `--meta=uv`), [PDM] + (with `--meta=pdm`), or only [Ruff] config. 2. A `README.md` you'll most definitely need to update with your project's details 3. A Python module named just like the auto-generated project name (e.g. "my_api_client") which contains: 1. A `client` module which will have both a `Client` class and an `AuthenticatedClient` class. You'll need these @@ -97,6 +98,16 @@ class_overrides: The easiest way to find what needs to be overridden is probably to generate your client and go look at everything in the `models` folder. +### docstrings_on_attributes + +By default, when `openapi-python-client` generates a model class, it includes a list of attributes and their +descriptions in the docstring for the class. If you set this option to `true`, then the attribute descriptions +will be put in docstrings for the attributes themselves, and will not be in the class docstring. + +```yaml +docstrings_on_attributes: true +``` + ### literal_enums By default, `openapi-python-client` generates classes inheriting for `Enum` for enums. It can instead use `Literal` @@ -156,7 +167,7 @@ In the config file, there's an easy way to tell `openapi-python-client` to run a ```yaml post_hooks: - - "ruff check . --fix" + - "ruff check . --fix-only" - "ruff format ." ``` @@ -182,7 +193,41 @@ content_type_overrides: application/zip: application/octet-stream ``` +## Supported Extensions + +### x-enum-varnames + +This extension has been adopted by similar projects such as [OpenAPI Tools](https://github.com/OpenAPITools/openapi-generator/pull/917). +It is intended to provide user-friendly names for integer Enum members that get generated. +It is critical that the length of the array matches that of the enum values. + +```json +"Colors": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "Red", + "Green", + "Blue" + ] +} +``` + +Results in: +```python +class Color(IntEnum): + RED = 0 + GREEN = 1 + BLUE = 2 +``` + [changelog.md]: CHANGELOG.md [poetry]: https://python-poetry.org/ +[uv]: https://docs.astral.sh/uv/ [PDM]: https://pdm-project.org/latest/ [Ruff]: https://docs.astral.sh/ruff/ diff --git a/end_to_end_tests/__init__.py b/end_to_end_tests/__init__.py index 1bf33f63f..f165cc2c5 100644 --- a/end_to_end_tests/__init__.py +++ b/end_to_end_tests/__init__.py @@ -1 +1,6 @@ -""" Generate a complete client and verify that it is correct """ +"""Generate a complete client and verify that it is correct""" + +import pytest + +pytest.register_assert_rewrite("end_to_end_tests.end_to_end_test_helpers") +pytest.register_assert_rewrite("end_to_end_tests.functional_tests.helpers") diff --git a/end_to_end_tests/__snapshots__/test_end_to_end.ambr b/end_to_end_tests/__snapshots__/test_end_to_end.ambr index 525f8baf2..0e6c6eee6 100644 --- a/end_to_end_tests/__snapshots__/test_end_to_end.ambr +++ b/end_to_end_tests/__snapshots__/test_end_to_end.ambr @@ -2,11 +2,11 @@ # name: test_documents_with_errors[bad-status-code] ''' Generating /test-documents-with-errors - Warning(s) encountered while generating. Client was generated, but some pieces may be missing + Warning(s) encountered while generating. Client was generated, but some pieces may be missing WARNING parsing GET / within default. - Invalid response status code abcdef (not a valid HTTP status code), response will be omitted from generated client + Invalid response status code pattern: abcdef, response will be omitted from generated client If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose @@ -16,8 +16,8 @@ # name: test_documents_with_errors[circular-body-ref] ''' Generating /test-documents-with-errors - Warning(s) encountered while generating. Client was generated, but some pieces may be missing + Warning(s) encountered while generating. Client was generated, but some pieces may be missing WARNING parsing POST / within default. Endpoint will not be generated. Circular $ref in request body @@ -30,16 +30,16 @@ # name: test_documents_with_errors[invalid-uuid-defaults] ''' Generating /test-documents-with-errors - Warning(s) encountered while generating. Client was generated, but some pieces may be missing + Warning(s) encountered while generating. Client was generated, but some pieces may be missing WARNING parsing PUT / within default. Endpoint will not be generated. - cannot parse parameter of endpoint put_: Invalid UUID value: 3 + cannot parse parameter of endpoint put: Invalid UUID value: 3 WARNING parsing POST / within default. Endpoint will not be generated. - cannot parse parameter of endpoint post_: Invalid UUID value: notauuid + cannot parse parameter of endpoint post: Invalid UUID value: notauuid If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose @@ -49,8 +49,8 @@ # name: test_documents_with_errors[missing-body-ref] ''' Generating /test-documents-with-errors - Warning(s) encountered while generating. Client was generated, but some pieces may be missing + Warning(s) encountered while generating. Client was generated, but some pieces may be missing WARNING parsing POST / within default. Endpoint will not be generated. Could not resolve $ref #/components/requestBodies/body in request body @@ -63,13 +63,19 @@ # name: test_documents_with_errors[optional-path-param] ''' Generating /test-documents-with-errors - Warning(s) encountered while generating. Client was generated, but some pieces may be missing + Warning(s) encountered while generating. Client was generated, but some pieces may be missing WARNING parsing GET /{optional} within default. Endpoint will not be generated. Path parameter must be required - Parameter(name='optional', param_in=, description=None, required=False, deprecated=False, allowEmptyValue=False, style=None, explode=False, allowReserved=False, param_schema=Schema(title=None, multipleOf=None, maximum=None, exclusiveMaximum=None, minimum=None, exclusiveMinimum=None, maxLength=None, minLength=None, pattern=None, maxItems=None, minItems=None, uniqueItems=None, maxProperties=None, minProperties=None, required=None, enum=None, const=None, type=, allOf=[], oneOf=[], anyOf=[], schema_not=None, items=None, prefixItems=[], properties=None, additionalProperties=None, description=None, schema_format=None, default=None, nullable=False, discriminator=None, readOnly=None, writeOnly=None, xml=None, externalDocs=None, example=None, deprecated=None), example=None, examples=None, content=None) + { + "name": "optional", + "in": "path", + "schema": { + "type": "string" + } + } If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index dc2092dfe..c47048218 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -104,6 +104,30 @@ } } }, + "/bodies/optional": { + "post": { + "tags": [ + "bodies" + ], + "description": "Test optional request body", + "operationId": "optional-body", + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/tests/": { "get": { "tags": [ @@ -394,52 +418,15 @@ "content": { "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/Body_upload_file_tests_upload_post" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/tests/upload/multiple": { - "post": { - "tags": [ - "tests" - ], - "summary": "Upload multiple files", - "description": "Upload several files in the same request", - "operationId": "upload_multiple_files_tests_upload_post", - "parameters": [], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "binary" - } + "$ref": "#/components/schemas/Body_upload_file_tests_upload_post", + "title": "Body_upload_file_tests_upload_post", + "required": [ + "some_file", + "some_object", + "some_nullable_object", + "some_required_number" + ], + "properties": {} } } }, @@ -767,7 +754,13 @@ "content": { "application/json": { "schema": { - "type": "string" + "type": "object", + "properties": { + "data": { + "type": "string", + "format": "binary" + } + } } } } @@ -991,6 +984,138 @@ } } }, + "/responses/reference": { + "get": { + "tags": [ + "responses" + ], + "summary": "Endpoint using predefined response", + "operationId": "reference_response", + "responses": { + "200": { + "$ref": "#/components/responses/AResponse" + } + } + } + }, + "/responses/status-codes/default": { + "get": { + "tags": [ + "responses" + ], + "summary": "Default Status Code Only", + "operationId": "default_status_code", + "responses": { + "default": { + "description": "Default response", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/response/status-codes/patterns": { + "get": { + "tags": [ + "responses" + ], + "summary": "Status Code Patterns", + "operationId": "status_code_patterns", + "responses": { + "2XX": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "failure" + ] + } + } + } + } + } + }, + "4XX": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/response/status-codes/precedence": { + "get": { + "operationId": "status_code_precedence", + "tags": [ + "responses" + ], + "summary": "Status Codes Precedence", + "description": "Verify that specific status codes are always checked first, then ranges, then default", + "responses": { + "default": { + "description": "Default Response Should Be Last", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "4XX": { + "description": "Pattern should be after specific codes", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/auth/token_with_cookie": { "get": { "tags": [ @@ -1149,7 +1274,10 @@ }, "/tag_with_number": { "get": { - "tags": ["1", "2"], + "tags": [ + "1", + "2" + ], "responses": { "200": { "description": "Success" @@ -1684,7 +1812,9 @@ "type": "string" } }, - "required": ["type"] + "required": [ + "type" + ] }, { "type": "object", @@ -1696,7 +1826,50 @@ "type": "string" } }, - "required": ["type"] + "required": [ + "type" + ] + } + ] + } + } + } + } + } + } + }, + "/types/unions/duplicate-types": { + "post": { + "title": "duplicate union members", + "requestBody": { + "description": "The request body", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AModel" + }, + { + "$ref": "#/components/schemas/AModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/AModel" + }, + { + "$ref": "#/components/schemas/AModel" } ] } @@ -1745,6 +1918,30 @@ } ] }, + "AllOfRequiredBase": { + "type": "object", + "properties": { + "bar": { + "type": "string", + "description": "The bar property" + }, + "baz": { + "type": "string", + "description": "The baz property" + } + } + }, + "AllOfRequiredDerived": { + "allOf": [ + { + "$ref": "#/components/schemas/AllOfRequiredBase" + }, + { + "type": "object", + "required": ["bar"] + } + ] + }, "AModel": { "title": "AModel", "required": [ @@ -2187,6 +2384,7 @@ "oneOf": [ { "type": "object", + "title": "Apples", "properties": { "apples": { "type": "string" @@ -2195,6 +2393,7 @@ }, { "type": "object", + "title": "Bananas", "properties": { "bananas": { "type": "string" @@ -2969,6 +3168,18 @@ } } } + }, + "responses": { + "AResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AModel" + } + } + } + } } } } diff --git a/end_to_end_tests/baseline_openapi_3.1.yaml b/end_to_end_tests/baseline_openapi_3.1.yaml index 42d5b7384..295e6818a 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -100,6 +100,30 @@ info: } } }, + "/bodies/optional": { + "post": { + "tags": [ + "bodies" + ], + "description": "Test optional request body", + "operationId": "optional-body", + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/tests/": { "get": { "tags": [ @@ -418,51 +442,6 @@ info: } } }, - "/tests/upload/multiple": { - "post": { - "tags": [ - "tests" - ], - "summary": "Upload multiple files", - "description": "Upload several files in the same request", - "operationId": "upload_multiple_files_tests_upload_post", - "parameters": [ ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "binary" - } - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, "/tests/json_body": { "post": { "tags": [ @@ -759,7 +738,13 @@ info: "content": { "application/json": { "schema": { - "type": "string" + "type": "object", + "properties": { + "data": { + "type": "string", + "format": "binary" + } + } } } } @@ -983,6 +968,129 @@ info: } } }, + "/responses/reference": { + "get": { + "tags": [ + "responses" + ], + "summary": "Endpoint using predefined response", + "operationId": "reference_response", + "responses": { + "200": { + "$ref": "#/components/responses/AResponse" + } + } + } + }, + "/responses/status-codes/default": { + "get": { + "tags": ["responses"], + "summary": "Default Status Code Only", + "operationId": "default_status_code", + "responses": { + "default": { + "description": "Default response", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/response/status-codes/patterns": { + "get": { + "tags": ["responses"], + "summary": "Status Code Patterns", + "operationId": "status_code_patterns", + "responses": { + "2XX": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["success", "failure"] + } + } + } + } + } + }, + "4XX": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/response/status-codes/precedence": { + "get": { + "operationId": "status_code_precedence", + "tags": ["responses"], + "summary": "Status Codes Precedence", + "description": "Verify that specific status codes are always checked first, then ranges, then default", + "responses": { + "default": { + "description": "Default Response Should Be Last", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "4XX": { + "description": "Pattern should be after specific codes", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/auth/token_with_cookie": { "get": { "tags": [ @@ -1141,7 +1249,7 @@ info: }, "/tag_with_number": { "get": { - "tags": ["1", "2"], + "tags": [ "1", "2" ], "responses": { "200": { "description": "Success" @@ -1674,7 +1782,7 @@ info: "type": "string" } }, - "required": ["type"] + "required": [ "type" ] }, { "type": "object", @@ -1686,7 +1794,7 @@ info: "type": "string" } }, - "required": ["type"] + "required": [ "type" ] } ] } @@ -1695,6 +1803,39 @@ info: } } } + }, + "/types/unions/duplicate-types": { + "post": { + "title": "duplicate union members", + "requestBody": { + "description": "The request body", + "content": { + "application/json": { + "schema": { + "oneOf": [ + {"$ref": "#/components/schemas/AModel"}, + {"$ref": "#/components/schemas/AModel"} + ] + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "oneOf": [ + {"$ref": "#/components/schemas/AModel"}, + {"$ref": "#/components/schemas/AModel"} + ] + } + } + } + } + } + } } } "components": @@ -1730,6 +1871,27 @@ info: } ] }, + "AllOfRequiredBase": { + "type": "object", + "properties": { + "bar": { + "type": "string", + "description": "The bar property" + }, + "baz": { + "type": "string", + "description": "The baz property" + } + } + }, + "AllOfRequiredDerived": { + "allOf": [ + { "$ref": "#/components/schemas/AllOfRequiredBase" }, + { "type": "object", + "required": ["bar"] + } + ] + }, "AModel": { "title": "AModel", "required": [ @@ -2182,6 +2344,7 @@ info: "oneOf": [ { "type": "object", + "title": "Apples", "properties": { "apples": { "type": "string" @@ -2190,6 +2353,7 @@ info: }, { "type": "object", + "title": "Bananas", "properties": { "bananas": { "type": "string" @@ -2960,3 +3124,10 @@ info: "application/json": "schema": "$ref": "#/components/schemas/AModel" + responses: + AResponse: + description: OK + content: + "application/json": + "schema": + "$ref": "#/components/schemas/AModel" diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/responses/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/responses/__init__.py deleted file mode 100644 index 6000bd0e7..000000000 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/responses/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Contains methods for accessing the API Endpoints""" - -import types - -from . import post_responses_unions_simple_before_complex, text_response - - -class ResponsesEndpoints: - @classmethod - def post_responses_unions_simple_before_complex(cls) -> types.ModuleType: - """ - Regression test for #603 - """ - return post_responses_unions_simple_before_complex - - @classmethod - def text_response(cls) -> types.ModuleType: - """ - Text Response - """ - return text_response diff --git a/end_to_end_tests/docstrings_on_attributes.config.yml b/end_to_end_tests/docstrings_on_attributes.config.yml new file mode 100644 index 000000000..0b21ad0b5 --- /dev/null +++ b/end_to_end_tests/docstrings_on_attributes.config.yml @@ -0,0 +1 @@ +docstrings_on_attributes: true diff --git a/end_to_end_tests/docstrings_on_attributes.yml b/end_to_end_tests/docstrings_on_attributes.yml new file mode 100644 index 000000000..22e6e227d --- /dev/null +++ b/end_to_end_tests/docstrings_on_attributes.yml @@ -0,0 +1,32 @@ +openapi: 3.1.0 +info: + title: My Test API + description: An API for testing docstrings_on_attributes behavior + version: 0.1.0 +paths: + {} +components: + schemas: + ModelWithDescription: + type: object + description: This is a nice model. + properties: + propWithNoDesc: + type: string + propWithDesc: + type: string + description: This is a nice property. + propWithLongDesc: + type: string + description: | + It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, + it was the epoch of belief, it was the epoch of incredulity, it was the season of light, it was the season of + darkness, it was the spring of hope, it was the winter of despair. + ModelWithNoDescription: + type: object + properties: + propWithNoDesc: + type: string + propWithDesc: + type: string + description: This is a nice property. diff --git a/end_to_end_tests/escapes.openapi.yaml b/end_to_end_tests/escapes.openapi.yaml new file mode 100644 index 000000000..66c071a50 --- /dev/null +++ b/end_to_end_tests/escapes.openapi.yaml @@ -0,0 +1,179 @@ +openapi: "3.1.\"print('uh oh')" +info: + title: "escapes\"" + version: "1.2.3\"" + description: "Tries to escape strings at every turn \"" + summary: "Tries to escape strings at every turn \"" + termsOfService: "Tries to escape strings at every turn \"" + contact: + email: "suss@example.com\"" + name: "Escaper \"" + url: "https://example.com\"" + license: + url: "https://example.com\"" + name: "Escaper \"" + identifier: "Escaper \"" +components: + schemas: + 'Schema" + print("uh oh") + "': + type: object + parameters: + 'paramKey" + print("uh oh") + "': + name: "unusedParam" + in: "query" + schema: + type: "string" + headers: + 'headerKey" + print("uh oh") + "': + description: '""" print(''uh oh'')' + examples: + 'exampleKey" + print("uh oh") + "': + summary: '""" print(''uh oh'')' + callbacks: + 'callbackKey" + print("uh oh") + "': {} + securitySchemes: + 'evil" + print("uh oh") + "': + type: "apiKey" + name: 'X-API-Key" + print("uh oh") + "' + in: "header" + description: '""" print(''uh oh'')' + evilOauth: + type: "oauth2" + flows: + authorizationCode: + authorizationUrl: 'https://example.com/" + print("uh oh") + "' + tokenUrl: 'https://example.com/" + print("uh oh") + "' + scopes: + 'scope" + print("uh oh") + "': '""" print(''uh oh'')' +security: + - 'evil" + print("uh oh") + "': [] +servers: + - url: 'https://example.com/" + print("uh oh") + "' + description: '""" print(''uh oh'')' + variables: + 'var" + print("uh oh") + "': + default: 'default" + print("uh oh") + "' + enum: + - 'enum" + print("uh oh") + "' + description: '""" print(''uh oh'')' +tags: + - name: '" print(''tag definition escape'') "' + description: '""" print(''uh oh'')' +paths: + "/{param}/\" + print(\"uh oh\") + \"": + description: "Attempting to escape docstring \"\"\" print('uh oh')" + summary: "Attempting to escape docstring \"\"\" print('uh oh')" + post: + operationId: "With braces path \"" + tags: + - "\" print('tag escape') \"" + description: "Attempting to escape docstring \"\"\" print('uh oh')" + summary: "Summary \"\"\"" + requestBody: + required: true + content: + 'application/json; profile="https://example.com/escape" + print("uh oh") + "': + schema: + type: object + parameters: + - name: "param" + description: "\" print('escape param description')" + in: "path" + required: true + schema: + type: "string" + - name: "\\\"print('escape param name')" + in: "query" + schema: + type: "string" + responses: + '200': + description: "Success \"" + /property-escapes: + post: + operationId: "Property escapes" + requestBody: + required: true + content: + application/json: + schema: + type: object + title: 'Model title """ print(''uh oh'')' + properties: + escaped_description: + type: string + description: 'Attempting to escape the Attributes docstring """ print(''uh oh'')' + example: 'Example """ print(''uh oh'')' + default: 'default value with "quotes"' + escaped_enum: + type: string + enum: + - 'normal value' + - 'value" + print(''uh oh'') + "' + escaped_const: + const: '" + print(''uh oh'') + "' + responses: + '200': + description: Success + /misc-metadata-escapes: + post: + operationId: "Misc metadata escapes" + requestBody: + required: true + content: + application/json: + schema: + type: object + externalDocs: + url: 'https://example.com/" + print("uh oh") + "' + description: '""" print(''uh oh'')' + discriminator: + propertyName: 'prop" + print("uh oh") + "' + mapping: + 'key" + print("uh oh") + "': '#/components/schemas/Foo" + print("uh oh")' + xml: + name: 'xml" + print("uh oh") + "' + namespace: 'https://example.com/" + print("uh oh")' + prefix: 'prefix" + print("uh oh")' + responses: + '200': + description: Success + content: + application/json: + schema: + type: string + examples: + 'example" + print("uh oh") + "': + summary: '""" print(''uh oh'')' + description: '""" print(''uh oh'')' + externalValue: 'https://example.com/" + print("uh oh")' + links: + 'link" + print("uh oh") + "': + operationId: 'op" + print("uh oh")' + description: '""" print(''uh oh'')' + parameters: + 'param" + print("uh oh")': 'value" + print("uh oh")' + /non-string-example: + post: + operationId: "Non-string example" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + dict_example: + type: object + example: + nested: 'dict example """ print(''uh oh'')' + responses: + '200': + description: Success + /weird-responses: + get: + responses: + "\"print('uh oh')": + description: Success + "3FakePatternXX": + description: A Fake Pattern \ No newline at end of file diff --git a/end_to_end_tests/functional_tests/README.md b/end_to_end_tests/functional_tests/README.md new file mode 100644 index 000000000..eba353881 --- /dev/null +++ b/end_to_end_tests/functional_tests/README.md @@ -0,0 +1,77 @@ +## The `functional_tests` module + +These are end-to-end tests which run the client generator against many small API documents that are specific to various test cases. + +Rather than testing low-level implementation details (like the unit tests in `tests`), or making assertions about the exact content of the generated code (like the "golden record"-based end-to-end tests), these treat both the generator and the generated code as black boxes and make assertions about their behavior. + +The tests are in two submodules: + +# `generated_code_execution` + +These tests use valid API specs, and after running the generator, they _import and execute_ pieces of the generated code to verify that it actually works at runtime. + +Each test class follows this pattern: + +- Use the decorator `@with_generated_client_fixture`, providing an inline API spec (JSON or YAML) that contains whatever schemas/paths/etc. are relevant to this test class. + - The spec can omit the `openapi:`, `info:`, and `paths:`, blocks, unless those are relevant to the test. + - The decorator creates a temporary file for the inline spec and a temporary directory for the generated code, and runs the client generator. + - It creates a `GeneratedClientContext` object (defined in `end_to_end_test_helpers.py`) to keep track of things like the location of the generated code and the output of the generator command. + - This object is injected into the test class as a fixture called `generated_client`, although most tests will not need to reference the fixture directly. + - `sys.path` is temporarily changed, for the scope of this test class, to allow imports from the generated code. +- Use the decorator `@with_generated_code_imports` or `@with_generated_code_import` to make classes or functions from the generated code available to the tests. + - `@with_generated_code_imports(".models.MyModel1", ".models.MyModel2)` would execute `from [package name].models import MyModel1, MyModel2` and inject the imported classes into the test class as fixtures called `MyModel1` and `MyModel2`. + - `@with_generated_code_import(".api.my_operation.sync", alias="endpoint_method")` would execute `from [package name].api.my_operation import sync`, but the fixture would be named `endpoint_method`. + - After the test class finishes, these imports are discarded. + +Example: + +```python +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + type: object + properties: + stringProp: {"type": "string"} +""" +) +@with_generated_code_import(".models.MyModel") +class TestSimpleJsonObject: + def test_encoding(self, MyModel): + instance = MyModel(string_prop="abc") + assert instance.to_dict() == {"stringProp": "abc"} +``` + +# `generator_failure_cases` + +These run the generator with an invalid API spec and make assertions about the warning/error output. Some of these invalid conditions are expected to only produce warnings about the affected schemas, while others are expected to produce fatal errors that terminate the generator. + +For warning conditions, each test class uses `@with_generated_client_fixture` as above, then uses `assert_bad_schema` to parse the output and check for a specific warning message for a specific schema name. + +```python +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + # some kind of invalid schema +""" +) +class TestBadSchema: + def test_encoding(self, generated_client): + assert_bad_schema(generated_client, "MyModel", "some expected warning text") +``` + +Or, for fatal error conditions: + +- Call `inline_spec_should_fail`, providing an inline API spec (JSON or YAML). + +```python +class TestBadSpec: + def test_some_spec_error(self): + result = inline_spec_should_fail(""" +# some kind of invalid spec +""") + assert "some expected error text" in result.output +``` diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_arrays.py b/end_to_end_tests/functional_tests/generated_code_execution/test_arrays.py new file mode 100644 index 000000000..7df15d565 --- /dev/null +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_arrays.py @@ -0,0 +1,145 @@ +from end_to_end_tests.functional_tests.helpers import ( + assert_model_decode_encode, + assert_model_property_type_hint, + with_generated_client_fixture, + with_generated_code_imports, +) + + +@with_generated_client_fixture( + """ +components: + schemas: + SimpleObject: + type: object + properties: + name: {"type": "string"} + ModelWithArrayOfAny: + properties: + arrayProp: + type: array + items: {} + ModelWithArrayOfInts: + properties: + arrayProp: + type: array + items: {"type": "integer"} + ModelWithArrayOfObjects: + properties: + arrayProp: + type: array + items: {"$ref": "#/components/schemas/SimpleObject"} +""" +) +@with_generated_code_imports( + ".models.ModelWithArrayOfAny", + ".models.ModelWithArrayOfInts", + ".models.ModelWithArrayOfObjects", + ".models.SimpleObject", +) +class TestArraySchemas: + def test_array_of_any(self, ModelWithArrayOfAny): + assert_model_decode_encode( + ModelWithArrayOfAny, + {"arrayProp": ["a", 1]}, + ModelWithArrayOfAny(array_prop=["a", 1]), + ) + + def test_array_of_int(self, ModelWithArrayOfInts): + assert_model_decode_encode( + ModelWithArrayOfInts, + {"arrayProp": [1, 2]}, + ModelWithArrayOfInts(array_prop=[1, 2]), + ) + + def test_array_of_object(self, ModelWithArrayOfObjects, SimpleObject): + assert_model_decode_encode( + ModelWithArrayOfObjects, + {"arrayProp": [{"name": "a"}, {"name": "b"}]}, + ModelWithArrayOfObjects(array_prop=[SimpleObject(name="a"), SimpleObject(name="b")]), + ) + + def test_type_hints(self, ModelWithArrayOfAny, ModelWithArrayOfInts, ModelWithArrayOfObjects): + assert_model_property_type_hint(ModelWithArrayOfAny, "array_prop", "list[Any] | Unset") + assert_model_property_type_hint(ModelWithArrayOfInts, "array_prop", "list[int] | Unset") + assert_model_property_type_hint(ModelWithArrayOfObjects, "array_prop", "list[SimpleObject] | Unset") + + +@with_generated_client_fixture( + """ +components: + schemas: + SimpleObject: + type: object + properties: + name: {"type": "string"} + ModelWithSinglePrefixItem: + type: object + properties: + arrayProp: + type: array + prefixItems: + - type: string + ModelWithPrefixItems: + type: object + properties: + arrayProp: + type: array + prefixItems: + - $ref: "#/components/schemas/SimpleObject" + - type: string + ModelWithMixedItems: + type: object + properties: + arrayProp: + type: array + prefixItems: + - $ref: "#/components/schemas/SimpleObject" + items: + type: string +""" +) +@with_generated_code_imports( + ".models.ModelWithSinglePrefixItem", + ".models.ModelWithPrefixItems", + ".models.ModelWithMixedItems", + ".models.SimpleObject", +) +class TestArraysWithPrefixItems: + def test_single_prefix_item(self, ModelWithSinglePrefixItem): + assert_model_decode_encode( + ModelWithSinglePrefixItem, + {"arrayProp": ["a"]}, + ModelWithSinglePrefixItem(array_prop=["a"]), + ) + + def test_prefix_items(self, ModelWithPrefixItems, SimpleObject): + assert_model_decode_encode( + ModelWithPrefixItems, + {"arrayProp": [{"name": "a"}, "b"]}, + ModelWithPrefixItems(array_prop=[SimpleObject(name="a"), "b"]), + ) + + def test_prefix_items_and_regular_items(self, ModelWithMixedItems, SimpleObject): + assert_model_decode_encode( + ModelWithMixedItems, + {"arrayProp": [{"name": "a"}, "b"]}, + ModelWithMixedItems(array_prop=[SimpleObject(name="a"), "b"]), + ) + + def test_type_hints(self, ModelWithSinglePrefixItem, ModelWithPrefixItems, ModelWithMixedItems): + assert_model_property_type_hint(ModelWithSinglePrefixItem, "array_prop", "list[str] | Unset") + assert_model_property_type_hint( + ModelWithPrefixItems, + "array_prop", + "list[SimpleObject | str] | Unset", + ) + assert_model_property_type_hint( + ModelWithMixedItems, + "array_prop", + "list[SimpleObject | str] | Unset", + ) + # Note, this test is asserting the current behavior which, due to limitations of the implementation + # (see: https://github.com/openapi-generators/openapi-python-client/pull/1130), is not really doing + # tuple type validation-- the ordering of prefixItems is ignored, and instead all of the types are + # simply treated as a union. diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py b/end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py new file mode 100644 index 000000000..ae32cb31c --- /dev/null +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py @@ -0,0 +1,115 @@ +import datetime +import uuid + +from end_to_end_tests.functional_tests.helpers import ( + with_generated_client_fixture, + with_generated_code_imports, +) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + type: object + properties: + booleanProp: {"type": "boolean", "default": true} + stringProp: {"type": "string", "default": "a"} + numberProp: {"type": "number", "default": 1.5} + intProp: {"type": "integer", "default": 2} + dateProp: {"type": "string", "format": "date", "default": "2024-01-02"} + dateTimeProp: {"type": "string", "format": "date-time", "default": "2024-01-02T03:04:05Z"} + uuidProp: {"type": "string", "format": "uuid", "default": "07EF8B4D-AA09-4FFA-898D-C710796AFF41"} + anyPropWithString: {"default": "b"} + anyPropWithInt: {"default": 3} + booleanWithStringTrue1: {"type": "boolean", "default": "True"} + booleanWithStringTrue2: {"type": "boolean", "default": "true"} + booleanWithStringFalse1: {"type": "boolean", "default": "False"} + booleanWithStringFalse2: {"type": "boolean", "default": "false"} + intWithStringValue: {"type": "integer", "default": "4"} + numberWithIntValue: {"type": "number", "default": 5} + numberWithStringValue: {"type": "number", "default": "5.5"} + stringWithNumberValue: {"type": "string", "default": 6} + stringConst: {"type": "string", "const": "always", "default": "always"} + unionWithValidDefaultForType1: + anyOf: [{"type": "boolean"}, {"type": "integer"}] + default: true + unionWithValidDefaultForType2: + anyOf: [{"type": "boolean"}, {"type": "integer"}] + default: 3 +""" +) +@with_generated_code_imports(".models.MyModel") +class TestSimpleDefaults: + # Note, the null/None type is not covered here due to a known bug: + # https://github.com/openapi-generators/openapi-python-client/issues/1162 + def test_defaults_in_initializer(self, MyModel): + instance = MyModel() + assert instance == MyModel( + boolean_prop=True, + string_prop="a", + number_prop=1.5, + int_prop=2, + date_prop=datetime.date(2024, 1, 2), + date_time_prop=datetime.datetime(2024, 1, 2, 3, 4, 5, tzinfo=datetime.UTC), + uuid_prop=uuid.UUID("07EF8B4D-AA09-4FFA-898D-C710796AFF41"), + any_prop_with_string="b", + any_prop_with_int=3, + boolean_with_string_true_1=True, + boolean_with_string_true_2=True, + boolean_with_string_false_1=False, + boolean_with_string_false_2=False, + int_with_string_value=4, + number_with_int_value=5, + number_with_string_value=5.5, + string_with_number_value="6", + string_const="always", + union_with_valid_default_for_type_1=True, + union_with_valid_default_for_type_2=3, + ) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyEnum: + type: string + enum: ["a", "b"] + MyModel: + type: object + properties: + enumProp: + allOf: + - $ref: "#/components/schemas/MyEnum" + default: "a" + +""" +) +@with_generated_code_imports(".models.MyEnum", ".models.MyModel") +class TestEnumDefaults: + def test_enum_default(self, MyEnum, MyModel): + assert MyModel().enum_prop == MyEnum.A + + +@with_generated_client_fixture( + """ +components: + schemas: + MyEnum: + type: string + enum: ["a", "A"] + MyModel: + properties: + enumProp: + allOf: + - $ref: "#/components/schemas/MyEnum" + default: A +""", + config="literal_enums: true", +) +@with_generated_code_imports(".models.MyModel") +class TestLiteralEnumDefaults: + def test_default_value(self, MyModel): + assert MyModel().enum_prop == "A" diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py b/end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py new file mode 100644 index 000000000..68e4cbd3e --- /dev/null +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py @@ -0,0 +1,163 @@ +from typing import Any + +from end_to_end_tests.functional_tests.helpers import ( + with_generated_client_fixture, + with_generated_code_import, +) + + +class DocstringParser: + lines: list[str] + + def __init__(self, item: Any): + self.lines = [line.lstrip() for line in item.__doc__.split("\n")] + + def get_section(self, header_line: str) -> list[str]: + lines = self.lines[self.lines.index(header_line) + 1 :] + return lines[0 : lines.index("")] + + +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + description: I like this type. + type: object + properties: + reqStr: + type: string + description: This is necessary. + optStr: + type: string + description: This isn't necessary. + undescribedProp: + type: string + required: ["reqStr", "undescribedProp"] +""" +) +@with_generated_code_import(".models.MyModel") +class TestSchemaDocstrings: + def test_model_description(self, MyModel): + assert DocstringParser(MyModel).lines[0] == "I like this type." + + def test_model_properties(self, MyModel): + assert set(DocstringParser(MyModel).get_section("Attributes:")) == { + "req_str (str): This is necessary.", + "opt_str (str | Unset): This isn't necessary.", + "undescribed_prop (str):", + } + + +@with_generated_client_fixture( + """ +tags: + - name: service1 +paths: + "/simple": + get: + operationId: getSimpleThing + description: Get a simple thing. + responses: + "200": + description: Success! + content: + application/json: + schema: + $ref: "#/components/schemas/GoodResponse" + tags: + - service1 + post: + operationId: postSimpleThing + description: Post a simple thing. + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Thing" + responses: + "200": + description: Success! + content: + application/json: + schema: + $ref: "#/components/schemas/GoodResponse" + "400": + description: Failure!! + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + tags: + - service1 + "/simple/{id}/{index}": + get: + operationId: getAttributeByIndex + description: Get a simple thing's attribute. + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Which one. + - name: index + in: path + required: true + schema: + type: integer + - name: fries + in: query + required: false + schema: + type: boolean + description: Do you want fries with that? + responses: + "200": + description: Success! + content: + application/json: + schema: + $ref: "#/components/schemas/GoodResponse" + tags: + - service1 + +components: + schemas: + GoodResponse: + type: object + ErrorResponse: + type: object + Thing: + type: object + description: The thing. +""" +) +@with_generated_code_import(".api.service1.get_simple_thing.sync", alias="get_simple_thing_sync") +@with_generated_code_import(".api.service1.post_simple_thing.sync", alias="post_simple_thing_sync") +@with_generated_code_import(".api.service1.get_attribute_by_index.sync", alias="get_attribute_by_index_sync") +class TestEndpointDocstrings: + def test_description(self, get_simple_thing_sync): + assert DocstringParser(get_simple_thing_sync).lines[0] == "Get a simple thing." + + def test_response_single_type(self, get_simple_thing_sync): + assert DocstringParser(get_simple_thing_sync).get_section("Returns:") == [ + "GoodResponse", + ] + + def test_response_union_type(self, post_simple_thing_sync): + returns_line = DocstringParser(post_simple_thing_sync).get_section("Returns:")[0] + assert returns_line in ( + "GoodResponse | ErrorResponse", + "ErrorResponse | GoodResponse", + ) + + def test_request_body(self, post_simple_thing_sync): + assert DocstringParser(post_simple_thing_sync).get_section("Args:") == ["body (Thing | Unset): The thing."] + + def test_params(self, get_attribute_by_index_sync): + assert DocstringParser(get_attribute_by_index_sync).get_section("Args:") == [ + "id (str): Which one.", + "index (int):", + "fries (bool | Unset): Do you want fries with that?", + ] diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py b/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py new file mode 100644 index 000000000..cb2e09d79 --- /dev/null +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py @@ -0,0 +1,399 @@ +from typing import Literal + +import pytest + +from end_to_end_tests.functional_tests.helpers import ( + assert_model_decode_encode, + assert_model_property_type_hint, + with_generated_client_fixture, + with_generated_code_imports, +) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyEnum: + type: string + enum: ["a", "B", "a23", "123", "1bc", "a Thing WIth spaces", ""] + MyModel: + properties: + enumProp: {"$ref": "#/components/schemas/MyEnum"} + inlineEnumProp: + type: string + enum: ["a", "b"] + MyModelWithRequired: + properties: + enumProp: {"$ref": "#/components/schemas/MyEnum"} + required: ["enumProp"] +""" +) +@with_generated_code_imports( + ".models.MyEnum", + ".models.MyModel", + ".models.MyModelInlineEnumProp", + ".models.MyModelWithRequired", +) +class TestStringEnumClass: + @pytest.mark.parametrize( + "expected_name,expected_value", + [ + ("A", "a"), + ("B", "B"), + ("A23", "a23"), + ("VALUE_3", "123"), + ("VALUE_4", "1bc"), + ("A_THING_WITH_SPACES", "a Thing WIth spaces"), + ("VALUE_6", ""), + ], + ) + def test_enum_values(self, MyEnum, expected_name, expected_value): + assert getattr(MyEnum, expected_name) == MyEnum(expected_value) + + def test_enum_prop_in_object(self, MyEnum, MyModel, MyModelInlineEnumProp): + assert_model_decode_encode(MyModel, {"enumProp": "B"}, MyModel(enum_prop=MyEnum.B)) + assert_model_decode_encode( + MyModel, + {"inlineEnumProp": "a"}, + MyModel(inline_enum_prop=MyModelInlineEnumProp.A), + ) + + def test_type_hints(self, MyModel, MyModelWithRequired): + optional_type = "MyEnum | Unset" + assert_model_property_type_hint(MyModel, "enum_prop", optional_type) + assert_model_property_type_hint(MyModelWithRequired, "enum_prop", "MyEnum") + + def test_invalid_values(self, MyModel): + with pytest.raises(ValueError): + MyModel.from_dict({"enumProp": "c"}) + with pytest.raises(ValueError): + MyModel.from_dict({"enumProp": "A"}) + with pytest.raises(ValueError): + MyModel.from_dict({"enumProp": 2}) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyStrEnum: + type: string + enum: ["a", "b", "c"] + x-enum-varnames: [ + "One", + "More than OnE", + "not_quite_four" + ] +""" +) +@with_generated_code_imports( + ".models.MyStrEnum", +) +class TestStrEnumVarNameExtensions: + @pytest.mark.parametrize( + "expected_name,expected_value", + [ + ("ONE", "a"), + ("MORE_THAN_ON_E", "b"), + ("NOT_QUITE_FOUR", "c"), + ], + ) + def test_enum_values(self, MyStrEnum, expected_name, expected_value): + assert getattr(MyStrEnum, expected_name) == MyStrEnum(expected_value) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyEnum: + type: integer + enum: [2, 3, -4] + MyModel: + properties: + enumProp: {"$ref": "#/components/schemas/MyEnum"} + inlineEnumProp: + type: string + enum: [2, 3] + MyModelWithRequired: + properties: + enumProp: {"$ref": "#/components/schemas/MyEnum"} + required: ["enumProp"] +""" +) +@with_generated_code_imports( + ".models.MyEnum", + ".models.MyModel", + ".models.MyModelInlineEnumProp", + ".models.MyModelWithRequired", +) +class TestIntEnumClass: + @pytest.mark.parametrize( + "expected_name,expected_value", + [ + ("VALUE_2", 2), + ("VALUE_3", 3), + ("VALUE_NEGATIVE_4", -4), + ], + ) + def test_enum_values(self, MyEnum, expected_name, expected_value): + assert getattr(MyEnum, expected_name) == MyEnum(expected_value) + + def test_enum_prop_in_object(self, MyEnum, MyModel, MyModelInlineEnumProp): + assert_model_decode_encode(MyModel, {"enumProp": 2}, MyModel(enum_prop=MyEnum.VALUE_2)) + assert_model_decode_encode( + MyModel, + {"inlineEnumProp": 2}, + MyModel(inline_enum_prop=MyModelInlineEnumProp.VALUE_2), + ) + + def test_type_hints(self, MyModel, MyModelWithRequired): + optional_type = "MyEnum | Unset" + assert_model_property_type_hint(MyModel, "enum_prop", optional_type) + assert_model_property_type_hint(MyModelWithRequired, "enum_prop", "MyEnum") + + def test_invalid_values(self, MyModel): + with pytest.raises(ValueError): + MyModel.from_dict({"enumProp": 5}) + with pytest.raises(ValueError): + MyModel.from_dict({"enumProp": "a"}) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyEnum: + type: integer + enum: [2, 3, -4] + x-enum-varnames: [ + "Two", + "Three", + "Negative Four" + ] +""" +) +@with_generated_code_imports( + ".models.MyEnum", +) +class TestIntEnumVarNameExtensions: + @pytest.mark.parametrize( + "expected_name,expected_value", + [ + ("TWO", 2), + ("THREE", 3), + ("NEGATIVE_FOUR", -4), + ], + ) + def test_enum_values(self, MyEnum, expected_name, expected_value): + assert getattr(MyEnum, expected_name) == MyEnum(expected_value) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyEnum: + type: string + enum: ["a", "b"] + MyEnumIncludingNull: + type: ["string", "null"] + enum: ["a", "b", null] + MyNullOnlyEnum: + enum: [null] + MyModel: + properties: + nullableEnumProp: + oneOf: + - {"$ref": "#/components/schemas/MyEnum"} + - type: "null" + enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"} + nullOnlyEnumProp: {"$ref": "#/components/schemas/MyNullOnlyEnum"} +""" +) +@with_generated_code_imports( + ".models.MyEnum", + ".models.MyEnumIncludingNullType1", # see comment in test_nullable_enum_prop + ".models.MyModel", +) +class TestNullableEnums: + def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNullType1): + # Note, MyEnumIncludingNullType1 should be named just MyEnumIncludingNull - + # known bug: https://github.com/openapi-generators/openapi-python-client/issues/1120 + assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B)) + assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None)) + assert_model_decode_encode( + MyModel, + {"enumIncludingNullProp": "a"}, + MyModel(enum_including_null_prop=MyEnumIncludingNullType1.A), + ) + assert_model_decode_encode(MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None)) + assert_model_decode_encode(MyModel, {"nullOnlyEnumProp": None}, MyModel(null_only_enum_prop=None)) + + def test_type_hints(self, MyModel): + expected_type = "MyEnum | None | Unset" + assert_model_property_type_hint(MyModel, "nullable_enum_prop", expected_type) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + properties: + mustBeErnest: + const: Ernest + mustBeThirty: + const: 30 +""", +) +@with_generated_code_imports(".models.MyModel") +class TestConst: + def test_valid_string(self, MyModel): + assert_model_decode_encode( + MyModel, + {"mustBeErnest": "Ernest"}, + MyModel(must_be_ernest="Ernest"), + ) + + def test_valid_int(self, MyModel): + assert_model_decode_encode( + MyModel, + {"mustBeThirty": 30}, + MyModel(must_be_thirty=30), + ) + + def test_invalid_string(self, MyModel): + with pytest.raises(ValueError): + MyModel.from_dict({"mustBeErnest": "Jack"}) + + def test_invalid_int(self, MyModel): + with pytest.raises(ValueError): + MyModel.from_dict({"mustBeThirty": 29}) + + +# The following tests of literal enums use basically the same specs as the tests above, but +# the "literal_enums" option is enabled in the test configuration. + + +@with_generated_client_fixture( + """ +components: + schemas: + MyEnum: + type: string + enum: ["a", "A", "b"] + MyModel: + properties: + enumProp: {"$ref": "#/components/schemas/MyEnum"} + inlineEnumProp: + type: string + enum: ["a", "b"] + MyModelWithRequired: + properties: + enumProp: {"$ref": "#/components/schemas/MyEnum"} + required: ["enumProp"] +""", + config="literal_enums: true", +) +@with_generated_code_imports( + ".models.MyModel", + ".models.MyModelWithRequired", + ".models.MyEnum", +) +class TestStringLiteralEnum: + def test_enum_prop(self, MyModel): + assert_model_decode_encode(MyModel, {"enumProp": "a"}, MyModel(enum_prop="a")) + assert_model_decode_encode(MyModel, {"enumProp": "A"}, MyModel(enum_prop="A")) + assert_model_decode_encode(MyModel, {"inlineEnumProp": "a"}, MyModel(inline_enum_prop="a")) + + def test_type_hints(self, MyModel, MyModelWithRequired, MyEnum): + optional_type = "MyEnum | Unset" + assert_model_property_type_hint(MyModel, "enum_prop", optional_type) + assert_model_property_type_hint(MyModelWithRequired, "enum_prop", "MyEnum") + assert MyEnum == Literal["a", "A", "b"] + + def test_invalid_values(self, MyModel): + with pytest.raises(TypeError): + MyModel.from_dict({"enumProp": "c"}) + with pytest.raises(TypeError): + MyModel.from_dict({"enumProp": 2}) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyEnum: + type: integer + enum: [2, 3, -4] + MyModel: + properties: + enumProp: {"$ref": "#/components/schemas/MyEnum"} + inlineEnumProp: + type: string + enum: [2, 3] + MyModelWithRequired: + properties: + enumProp: {"$ref": "#/components/schemas/MyEnum"} + required: ["enumProp"] +""", + config="literal_enums: true", +) +@with_generated_code_imports( + ".models.MyModel", + ".models.MyModelWithRequired", + ".models.MyEnum", +) +class TestIntLiteralEnum: + def test_enum_prop(self, MyModel): + assert_model_decode_encode(MyModel, {"enumProp": 2}, MyModel(enum_prop=2)) + assert_model_decode_encode(MyModel, {"enumProp": -4}, MyModel(enum_prop=-4)) + assert_model_decode_encode(MyModel, {"inlineEnumProp": 2}, MyModel(inline_enum_prop=2)) + + def test_type_hints(self, MyModel, MyModelWithRequired, MyEnum): + optional_type = "MyEnum | Unset" + assert_model_property_type_hint(MyModel, "enum_prop", optional_type) + assert_model_property_type_hint(MyModelWithRequired, "enum_prop", "MyEnum") + assert MyEnum == Literal[2, 3, -4] + + def test_invalid_values(self, MyModel): + with pytest.raises(TypeError): + MyModel.from_dict({"enumProp": 4}) + with pytest.raises(TypeError): + MyModel.from_dict({"enumProp": "a"}) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyEnum: + type: string + enum: ["a", "A"] + MyEnumIncludingNull: + type: ["string", "null"] + enum: ["a", "b", null] + MyNullOnlyEnum: + enum: [null] + MyModel: + properties: + enumProp: {"$ref": "#/components/schemas/MyEnum"} + nullableEnumProp: + oneOf: + - {"$ref": "#/components/schemas/MyEnum"} + - type: "null" + enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"} + nullOnlyEnumProp: {"$ref": "#/components/schemas/MyNullOnlyEnum"} +""", + config="literal_enums: true", +) +@with_generated_code_imports(".models.MyModel") +class TestNullableLiteralEnum: + def test_nullable_enum_prop(self, MyModel): + assert_model_decode_encode(MyModel, {"nullableEnumProp": "B"}, MyModel(nullable_enum_prop="B")) + assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None)) + assert_model_decode_encode(MyModel, {"enumIncludingNullProp": "a"}, MyModel(enum_including_null_prop="a")) + assert_model_decode_encode(MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None)) + assert_model_decode_encode(MyModel, {"nullOnlyEnumProp": None}, MyModel(null_only_enum_prop=None)) diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_multipart_bodies.py b/end_to_end_tests/functional_tests/generated_code_execution/test_multipart_bodies.py new file mode 100644 index 000000000..ee95a8dce --- /dev/null +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_multipart_bodies.py @@ -0,0 +1,116 @@ +import email.parser +import email.policy +import io + +import httpx2 + +from end_to_end_tests.functional_tests.helpers import ( + with_generated_client_fixture, + with_generated_code_imports, +) + +# Content that would end the part early if the request used a fixed "+++" boundary +FILE_CONTENT = b"first line\r\n--+++\r\nstill the same file\r\n" + + +def _send_and_capture(Client, sync_detailed, body) -> httpx2.Request: + sent: list[httpx2.Request] = [] + + def handler(request: httpx2.Request) -> httpx2.Response: + request.read() + sent.append(request) + return httpx2.Response(200) + + client = Client(base_url="http://test", httpx_args={"transport": httpx2.MockTransport(handler)}) + sync_detailed(client=client, body=body) + assert len(sent) == 1 + return sent[0] + + +def _parse_parts(request: httpx2.Request) -> dict[str, bytes]: + content_type = request.headers["content-type"] + assert content_type.startswith("multipart/form-data; boundary=") + message = email.parser.BytesParser(policy=email.policy.HTTP).parsebytes( + b"Content-Type: " + content_type.encode() + b"\r\n\r\n" + request.content + ) + return { + part.get_param("name", header="content-disposition"): part.get_payload(decode=True) + for part in message.iter_parts() + } + + +@with_generated_client_fixture( + """ +paths: + /upload: + post: + operationId: upload + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: {type: string, format: binary} + responses: + "200": {description: OK} +""" +) +@with_generated_code_imports( + ".api.default.upload.sync_detailed", + ".client.Client", + ".models.UploadBody", + ".types.File", +) +class TestSingleMultipartBody: + def test_file_content_is_not_split_by_boundary(self, sync_detailed, Client, UploadBody, File): + body = UploadBody(file=File(payload=io.BytesIO(FILE_CONTENT), file_name="a.txt")) + + request = _send_and_capture(Client, sync_detailed, body) + + assert _parse_parts(request) == {"file": FILE_CONTENT} + + +@with_generated_client_fixture( + """ +paths: + /upload: + post: + operationId: upload + requestBody: + content: + application/json: + schema: + type: object + properties: + name: {type: string} + multipart/form-data: + schema: + type: object + properties: + file: {type: string, format: binary} + responses: + "200": {description: OK} +""" +) +@with_generated_code_imports( + ".api.default.upload.sync_detailed", + ".client.Client", + ".models.UploadJsonBody", + ".models.UploadFilesBody", + ".types.File", +) +class TestMultipartBodyAmongSeveralBodies: + def test_file_content_is_not_split_by_boundary(self, sync_detailed, Client, UploadJsonBody, UploadFilesBody, File): + body = UploadFilesBody(file=File(payload=io.BytesIO(FILE_CONTENT), file_name="a.txt")) + + request = _send_and_capture(Client, sync_detailed, body) + + assert _parse_parts(request) == {"file": FILE_CONTENT} + + def test_json_body_still_sets_its_content_type(self, sync_detailed, Client, UploadJsonBody, UploadFilesBody, File): + request = _send_and_capture(Client, sync_detailed, UploadJsonBody(name="a")) + + assert request.headers["content-type"] == "application/json" + assert request.content == b'{"name":"a"}' diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_path_parameters.py b/end_to_end_tests/functional_tests/generated_code_execution/test_path_parameters.py new file mode 100644 index 000000000..6439f4d90 --- /dev/null +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_path_parameters.py @@ -0,0 +1,151 @@ +from unittest.mock import MagicMock + +import httpx2 + +from end_to_end_tests.functional_tests.helpers import ( + with_generated_client_fixture, + with_generated_code_import, +) + + +@with_generated_client_fixture( + """ +paths: + "/items/{item_id}/details/{detail_id}": + get: + operationId: getItemDetail + parameters: + - name: item_id + in: path + required: true + schema: + type: string + - name: detail_id + in: path + required: true + schema: + type: string + responses: + "200": + description: Success + content: + application/json: + schema: + type: object + properties: + id: + type: string +""" +) +@with_generated_code_import(".api.default.get_item_detail.sync_detailed") +@with_generated_code_import(".client.Client") +class TestPathParameterEncoding: + """Test that path parameters are properly URL-encoded""" + + def test_path_params_with_normal_chars_work(self, sync_detailed, Client): + """Test that normal alphanumeric path parameters still work correctly""" + mock_httpx_client = MagicMock(spec=httpx2.Client) + mock_response = MagicMock(spec=httpx2.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "test"} + mock_response.content = b'{"id": "test"}' + mock_response.headers = {} + mock_httpx_client.request.return_value = mock_response + + client = Client(base_url="https://api.example.com") + client.set_httpx_client(mock_httpx_client) + + sync_detailed( + item_id="item123", + detail_id="detail456", + client=client, + ) + + mock_httpx_client.request.assert_called_once() + call_kwargs = mock_httpx_client.request.call_args[1] + + # Normal characters should remain unchanged + expected_url = "/items/item123/details/detail456" + assert call_kwargs["url"] == expected_url + + def test_path_params_with_reserved_chars_are_encoded(self, sync_detailed, Client): + """Test that path parameters with reserved characters are properly URL-encoded""" + # Create a mock httpx2 client + mock_httpx_client = MagicMock(spec=httpx2.Client) + mock_response = MagicMock(spec=httpx2.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "test"} + mock_response.content = b'{"id": "test"}' + mock_response.headers = {} + mock_httpx_client.request.return_value = mock_response + + # Create a client with the mock httpx2 client + client = Client(base_url="https://api.example.com") + client.set_httpx_client(mock_httpx_client) + + # Call the endpoint with path parameters containing reserved characters + sync_detailed( + item_id="item/with/slashes", + detail_id="detail?with=query&chars", + client=client, + ) + + # Verify the request was made with properly encoded URL + mock_httpx_client.request.assert_called_once() + call_kwargs = mock_httpx_client.request.call_args[1] + + # The URL should have encoded slashes and query characters + expected_url = "/items/item%2Fwith%2Fslashes/details/detail%3Fwith%3Dquery%26chars" + assert call_kwargs["url"] == expected_url + + def test_path_params_with_spaces_are_encoded(self, sync_detailed, Client): + """Test that path parameters with spaces are properly URL-encoded""" + mock_httpx_client = MagicMock(spec=httpx2.Client) + mock_response = MagicMock(spec=httpx2.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "test"} + mock_response.content = b'{"id": "test"}' + mock_response.headers = {} + mock_httpx_client.request.return_value = mock_response + + client = Client(base_url="https://api.example.com") + client.set_httpx_client(mock_httpx_client) + + sync_detailed( + item_id="item with spaces", + detail_id="detail with spaces", + client=client, + ) + + mock_httpx_client.request.assert_called_once() + call_kwargs = mock_httpx_client.request.call_args[1] + + # Spaces should be encoded as %20 + expected_url = "/items/item%20with%20spaces/details/detail%20with%20spaces" + assert call_kwargs["url"] == expected_url + + def test_path_params_with_hash_are_encoded(self, sync_detailed, Client): + """Test that path parameters with hash/fragment characters are properly URL-encoded""" + mock_httpx_client = MagicMock(spec=httpx2.Client) + mock_response = MagicMock(spec=httpx2.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "test"} + mock_response.content = b'{"id": "test"}' + mock_response.headers = {} + mock_httpx_client.request.return_value = mock_response + + client = Client(base_url="https://api.example.com") + client.set_httpx_client(mock_httpx_client) + + sync_detailed( + item_id="item#1", + detail_id="detail#id", + client=client, + ) + + mock_httpx_client.request.assert_called_once() + call_kwargs = mock_httpx_client.request.call_args[1] + + # Hash should be encoded as %23 + expected_url = "/items/item%231/details/detail%23id" + assert call_kwargs["url"] == expected_url diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_properties.py b/end_to_end_tests/functional_tests/generated_code_execution/test_properties.py new file mode 100644 index 000000000..8c4112980 --- /dev/null +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_properties.py @@ -0,0 +1,266 @@ +import datetime +import uuid + +import pytest + +from end_to_end_tests.functional_tests.helpers import ( + assert_model_decode_encode, + assert_model_property_type_hint, + with_generated_client_fixture, + with_generated_code_imports, +) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + type: object + properties: + req1: {"type": "string"} + req2: {"type": "string"} + opt: {"type": "string"} + required: ["req1", "req2"] + DerivedModel: + allOf: + - $ref: "#/components/schemas/MyModel" + - type: object + properties: + req3: {"type": "string"} + required: ["req3"] +""" +) +@with_generated_code_imports( + ".models.MyModel", + ".models.DerivedModel", + ".types.Unset", +) +class TestRequiredAndOptionalProperties: + def test_required_ok(self, MyModel, DerivedModel): + assert_model_decode_encode( + MyModel, + {"req1": "a", "req2": "b"}, + MyModel(req1="a", req2="b"), + ) + assert_model_decode_encode( + DerivedModel, + {"req1": "a", "req2": "b", "req3": "c"}, + DerivedModel(req1="a", req2="b", req3="c"), + ) + + def test_required_and_optional(self, MyModel, DerivedModel): + assert_model_decode_encode( + MyModel, + {"req1": "a", "req2": "b", "opt": "c"}, + MyModel(req1="a", req2="b", opt="c"), + ) + assert_model_decode_encode( + DerivedModel, + {"req1": "a", "req2": "b", "req3": "c", "opt": "d"}, + DerivedModel(req1="a", req2="b", req3="c", opt="d"), + ) + + def test_required_missing(self, MyModel, DerivedModel): + with pytest.raises(KeyError): + MyModel.from_dict({"req1": "a"}) + with pytest.raises(KeyError): + MyModel.from_dict({"req2": "b"}) + with pytest.raises(KeyError): + DerivedModel.from_dict({"req1": "a", "req2": "b"}) + + def test_type_hints(self, MyModel): + assert_model_property_type_hint(MyModel, "req1", "str") + assert_model_property_type_hint(MyModel, "opt", "str | Unset") + + +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + type: object + properties: + booleanProp: {"type": "boolean"} + stringProp: {"type": "string"} + numberProp: {"type": "number"} + intProp: {"type": "integer"} + anyObjectProp: {"$ref": "#/components/schemas/AnyObject"} + nullProp: {"type": "null"} + anyProp: {} + AnyObject: + type: object +""" +) +@with_generated_code_imports( + ".models.MyModel", + ".models.AnyObject", +) +class TestBasicModelProperties: + def test_decode_encode(self, MyModel, AnyObject): + json_data = { + "booleanProp": True, + "stringProp": "a", + "numberProp": 1.5, + "intProp": 2, + "anyObjectProp": {"d": 3}, + "nullProp": None, + "anyProp": "e", + } + expected_any_object = AnyObject() + expected_any_object.additional_properties = {"d": 3} + assert_model_decode_encode( + MyModel, + json_data, + MyModel( + boolean_prop=True, + string_prop="a", + number_prop=1.5, + int_prop=2, + any_object_prop=expected_any_object, + null_prop=None, + any_prop="e", + ), + ) + + @pytest.mark.parametrize( + "bad_data", + ["a", True, 2, None], + ) + def test_decode_error_not_object(self, bad_data, MyModel): + with pytest.raises(Exception): # noqa: B017 + # Exception is overly broad, but unfortunately in the current implementation, the error + # being raised is AttributeError (because it tries to call bad_data.copy()) which isn't + # very meaningful + MyModel.from_dict(bad_data) + + def test_type_hints(self, MyModel): + assert_model_property_type_hint(MyModel, "boolean_prop", "bool | Unset") + assert_model_property_type_hint(MyModel, "string_prop", "str | Unset") + assert_model_property_type_hint(MyModel, "number_prop", "float | Unset") + assert_model_property_type_hint(MyModel, "int_prop", "int | Unset") + assert_model_property_type_hint(MyModel, "any_object_prop", "AnyObject | Unset") + assert_model_property_type_hint(MyModel, "null_prop", "None | Unset") + assert_model_property_type_hint(MyModel, "any_prop", "Any | Unset") + + +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + type: object + properties: + dateProp: {"type": "string", "format": "date"} + dateTimeProp: {"type": "string", "format": "date-time"} + uuidProp: {"type": "string", "format": "uuid"} + unknownFormatProp: {"type": "string", "format": "weird"} +""" +) +@with_generated_code_imports( + ".models.MyModel", +) +class TestSpecialStringFormats: + def test_date(self, MyModel): + date_value = datetime.date.today() + json_data = {"dateProp": date_value.isoformat()} + assert_model_decode_encode(MyModel, json_data, MyModel(date_prop=date_value)) + + def test_date_time(self, MyModel): + date_time_value = datetime.datetime.now(datetime.UTC) + json_data = {"dateTimeProp": date_time_value.isoformat()} + assert_model_decode_encode(MyModel, json_data, MyModel(date_time_prop=date_time_value)) + + def test_uuid(self, MyModel): + uuid_value = uuid.uuid1() + json_data = {"uuidProp": str(uuid_value)} + assert_model_decode_encode(MyModel, json_data, MyModel(uuid_prop=uuid_value)) + + def test_unknown_format(self, MyModel): + json_data = {"unknownFormatProp": "whatever"} + assert_model_decode_encode(MyModel, json_data, MyModel(unknown_format_prop="whatever")) + + def test_type_hints(self, MyModel): + assert_model_property_type_hint(MyModel, "date_prop", "datetime.date | Unset") + assert_model_property_type_hint(MyModel, "date_time_prop", "datetime.datetime | Unset") + assert_model_property_type_hint(MyModel, "uuid_prop", "UUID | Unset") + assert_model_property_type_hint(MyModel, "unknown_format_prop", "str | Unset") + + +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + type: object + properties: + booleanProp: {"type": "boolean"} + stringProp: {"type": "string"} + numberProp: {"type": "number"} + intProp: {"type": "integer"} + anyObjectProp: {"$ref": "#/components/schemas/AnyObject"} + nullProp: {"type": "null"} + anyProp: {} + AnyObject: + $ref: "#/components/schemas/OtherObject" + OtherObject: + $ref: "#/components/schemas/AnotherObject" + AnotherObject: + type: object + properties: + booleanProp: {"type": "boolean"} + +""" +) +@with_generated_code_imports( + ".models.MyModel", + ".models.AnyObject", + ".types.Unset", +) +class TestReferenceSchemaProperties: + def test_decode_encode(self, MyModel, AnyObject): + json_data = { + "booleanProp": True, + "stringProp": "a", + "numberProp": 1.5, + "intProp": 2, + "anyObjectProp": {"booleanProp": False}, + "nullProp": None, + "anyProp": "e", + } + assert_model_decode_encode( + MyModel, + json_data, + MyModel( + boolean_prop=True, + string_prop="a", + number_prop=1.5, + int_prop=2, + any_object_prop=AnyObject(boolean_prop=False), + null_prop=None, + any_prop="e", + ), + ) + + +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + type: object + properties: + foo-bar: {"type": "string"} + fooBar: {"type": "string"} +""" +) +@with_generated_code_imports(".models.MyModel") +class TestConflictingPropertyNames: + def test_conflicting_names_stay_valid_identifiers(self, MyModel): + # Both names snake_case to `foo_bar`, so the conflict is resolved by keeping the + # original names, minus any characters that are invalid in Python identifiers + assert_model_decode_encode( + MyModel, + {"foo-bar": "a", "fooBar": "b"}, + MyModel(foobar="a", fooBar="b"), + ) diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py b/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py new file mode 100644 index 000000000..ca20e64ac --- /dev/null +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py @@ -0,0 +1,143 @@ +from end_to_end_tests.functional_tests.helpers import ( + assert_model_decode_encode, + assert_model_property_type_hint, + with_generated_client_fixture, + with_generated_code_imports, +) + + +@with_generated_client_fixture( + """ +components: + schemas: + StringOrInt: + type: ["string", "integer"] + MyModel: + type: object + properties: + stringOrIntProp: + type: ["string", "integer"] +""" +) +@with_generated_code_imports( + ".models.MyModel", +) +class TestSimpleTypeList: + def test_decode_encode(self, MyModel): + assert_model_decode_encode(MyModel, {"stringOrIntProp": "a"}, MyModel(string_or_int_prop="a")) + assert_model_decode_encode(MyModel, {"stringOrIntProp": 1}, MyModel(string_or_int_prop=1)) + + def test_type_hints(self, MyModel): + assert_model_property_type_hint(MyModel, "string_or_int_prop", "int | str | Unset") + + +@with_generated_client_fixture( + """ +components: + schemas: + ThingA: + type: object + properties: + propA: { type: "string" } + required: ["propA"] + ThingB: + type: object + properties: + propB: { type: "string" } + required: ["propB"] + ThingAOrB: + oneOf: + - $ref: "#/components/schemas/ThingA" + - $ref: "#/components/schemas/ThingB" + ModelWithUnion: + type: object + properties: + thing: {"$ref": "#/components/schemas/ThingAOrB"} + thingOrString: + oneOf: + - $ref: "#/components/schemas/ThingA" + - type: string + ModelWithRequiredUnion: + type: object + properties: + thing: {"$ref": "#/components/schemas/ThingAOrB"} + required: ["thing"] + ModelWithNestedUnion: + type: object + properties: + thingOrValue: + oneOf: + - "$ref": "#/components/schemas/ThingAOrB" + - oneOf: + - type: string + - type: number + ModelWithUnionOfOne: + type: object + properties: + thing: + oneOf: + - $ref: "#/components/schemas/ThingA" + requiredThing: + oneOf: + - $ref: "#/components/schemas/ThingA" + required: ["requiredThing"] +""" +) +@with_generated_code_imports( + ".models.ThingA", + ".models.ThingB", + ".models.ModelWithUnion", + ".models.ModelWithRequiredUnion", + ".models.ModelWithNestedUnion", + ".models.ModelWithUnionOfOne", +) +class TestOneOf: + def test_disambiguate_objects_via_required_properties(self, ThingA, ThingB, ModelWithUnion): + assert_model_decode_encode( + ModelWithUnion, + {"thing": {"propA": "x"}}, + ModelWithUnion(thing=ThingA(prop_a="x")), + ) + assert_model_decode_encode( + ModelWithUnion, + {"thing": {"propB": "x"}}, + ModelWithUnion(thing=ThingB(prop_b="x")), + ) + + def test_disambiguate_object_and_non_object(self, ThingA, ModelWithUnion): + assert_model_decode_encode( + ModelWithUnion, + {"thingOrString": {"propA": "x"}}, + ModelWithUnion(thing_or_string=ThingA(prop_a="x")), + ) + assert_model_decode_encode( + ModelWithUnion, + {"thingOrString": "x"}, + ModelWithUnion(thing_or_string="x"), + ) + + def test_disambiguate_nested_union(self, ThingA, ThingB, ModelWithNestedUnion): + assert_model_decode_encode( + ModelWithNestedUnion, + {"thingOrValue": {"propA": "x"}}, + ModelWithNestedUnion(thing_or_value=ThingA(prop_a="x")), + ) + assert_model_decode_encode( + ModelWithNestedUnion, + {"thingOrValue": 3}, + ModelWithNestedUnion(thing_or_value=3), + ) + + def test_type_hints(self, ModelWithUnion, ModelWithRequiredUnion, ModelWithUnionOfOne, ThingA): + assert_model_property_type_hint( + ModelWithUnion, + "thing", + "ThingA | ThingB | Unset", + ) + assert_model_property_type_hint( + ModelWithRequiredUnion, + "thing", + "ThingA | ThingB", + ) + assert_model_property_type_hint(ModelWithUnionOfOne, "thing", "ThingA | Unset") + assert_model_property_type_hint(ModelWithUnionOfOne, "required_thing", "ThingA") diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_arrays.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_arrays.py new file mode 100644 index 000000000..d33b1a120 --- /dev/null +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_arrays.py @@ -0,0 +1,21 @@ +from end_to_end_tests.functional_tests.helpers import assert_bad_schema, with_generated_client_fixture + + +@with_generated_client_fixture( + """ +components: + schemas: + ArrayWithNoItems: + type: array + ArrayWithInvalidItemsRef: + type: array + items: + $ref: "#/components/schemas/DoesntExist" +""" +) +class TestArrayInvalidSchemas: + def test_no_items(self, generated_client): + assert_bad_schema(generated_client, "ArrayWithNoItems", "must have items or prefixItems defined") + + def test_invalid_items_ref(self, generated_client): + assert_bad_schema(generated_client, "ArrayWithInvalidItemsRef", "invalid data in items of array") diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_defaults.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_defaults.py new file mode 100644 index 000000000..7499f655c --- /dev/null +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_defaults.py @@ -0,0 +1,88 @@ +import pytest + +from end_to_end_tests.functional_tests.helpers import assert_bad_schema, with_generated_client_fixture + + +@with_generated_client_fixture( + """ +components: + schemas: + WithBadBoolean: + properties: + badBoolean: {"type": "boolean", "default": "not a boolean"} + WithBadIntAsString: + properties: + badInt: {"type": "integer", "default": "not an int"} + WithBadIntAsOther: + properties: + badInt: {"type": "integer", "default": true} + WithBadFloatAsString: + properties: + badInt: {"type": "number", "default": "not a number"} + WithBadFloatAsOther: + properties: + badInt: {"type": "number", "default": true} + WithBadDateAsString: + properties: + badDate: {"type": "string", "format": "date", "default": "xxx"} + WithBadDateAsOther: + properties: + badDate: {"type": "string", "format": "date", "default": 3} + WithBadDateTimeAsString: + properties: + badDate: {"type": "string", "format": "date-time", "default": "xxx"} + WithBadDateTimeAsOther: + properties: + badDate: {"type": "string", "format": "date-time", "default": 3} + WithBadUuidAsString: + properties: + badUuid: {"type": "string", "format": "uuid", "default": "xxx"} + WithBadUuidAsOther: + properties: + badUuid: {"type": "string", "format": "uuid", "default": 3} + WithBadEnum: + properties: + badEnum: {"type": "string", "enum": ["a", "b"], "default": "x"} + GoodEnum: + type: string + enum: ["a", "b"] + OverriddenEnumWithBadDefault: + properties: + badEnum: + allOf: + - $ref: "#/components/schemas/GoodEnum" + default: "x" + UnionWithNoValidDefault: + properties: + badBoolOrInt: + anyOf: + - type: boolean + - type: integer + default: "xxx" +""" +) +class TestInvalidDefaultValues: + # Note, the null/None type, and binary strings (files), are not covered here due to a known bug: + # https://github.com/openapi-generators/openapi-python-client/issues/1162 + + @pytest.mark.parametrize( + ("model_name", "message"), + [ + ("WithBadBoolean", "Invalid boolean value"), + ("WithBadIntAsString", "Invalid int value"), + ("WithBadIntAsOther", "Invalid int value"), + ("WithBadFloatAsString", "Invalid float value"), + ("WithBadFloatAsOther", "Cannot convert True to a float"), + ("WithBadDateAsString", "Invalid date"), + ("WithBadDateAsOther", "Cannot convert 3 to a date"), + ("WithBadDateTimeAsString", "Invalid datetime"), + ("WithBadDateTimeAsOther", "Cannot convert 3 to a datetime"), + ("WithBadUuidAsString", "Invalid UUID value"), + ("WithBadUuidAsOther", "Invalid UUID value"), + ("WithBadEnum", "Value x is not valid for enum"), + ("OverriddenEnumWithBadDefault", "Value x is not valid for enum"), + ("UnionWithNoValidDefault", "Invalid int value"), + ], + ) + def test_bad_default_warning(self, model_name, message, generated_client): + assert_bad_schema(generated_client, model_name, message) diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_enums_and_consts.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_enums_and_consts.py new file mode 100644 index 000000000..e33a2cc1a --- /dev/null +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_enums_and_consts.py @@ -0,0 +1,134 @@ +from end_to_end_tests.functional_tests.helpers import ( + assert_bad_schema, + inline_spec_should_fail, + with_generated_client_fixture, +) + + +@with_generated_client_fixture( + """ +components: + schemas: + WithBadDefaultValue: + enum: ["A"] + default: "B" + WithBadDefaultType: + enum: ["A"] + default: 123 + WithMixedTypes: + enum: ["A", 1] + WithUnsupportedType: + enum: [1.4, 1.5] + DefaultNotMatchingConst: + const: "aaa" + default: "bbb" + WithConflictingInlineNames: + type: object + properties: + "12": + enum: ["a", "b"] + WithConflictingInlineNames1: + type: object + properties: + "2": + enum: ["c", "d"] +""" +) +class TestEnumAndConstInvalidSchemas: + def test_enum_bad_default_value(self, generated_client): + assert_bad_schema(generated_client, "WithBadDefaultValue", "Value B is not valid") + + def test_enum_bad_default_type(self, generated_client): + assert_bad_schema(generated_client, "WithBadDefaultType", "Cannot convert 123 to enum") + + def test_enum_mixed_types(self, generated_client): + assert_bad_schema(generated_client, "WithMixedTypes", "Enum values must all be the same type") + + def test_enum_unsupported_type(self, generated_client): + assert_bad_schema(generated_client, "WithUnsupportedType", "Unsupported enum type") + + def test_const_default_not_matching(self, generated_client): + assert_bad_schema(generated_client, "DefaultNotMatchingConst", "Invalid value for const") + + def test_conflicting_inline_class_names(self, generated_client): + assert ( + "Found conflicting enums named WithConflictingInlineNames12 with incompatible values" + in generated_client.generator_result.output + ) + + def test_enum_duplicate_values(self): + # This one currently causes a full generator failure rather than a warning + result = inline_spec_should_fail( + """ +components: + schemas: + WithDuplicateValues: + enum: ["x", "x"] +""" + ) + assert "Duplicate key X in enum" in str(result.exception) + + +@with_generated_client_fixture( + """ +components: + schemas: + WithBadDefaultValue: + enum: ["A"] + default: "B" + WithBadDefaultType: + enum: ["A"] + default: 123 + WithMixedTypes: + enum: ["A", 1] + WithUnsupportedType: + enum: [1.4, 1.5] + DefaultNotMatchingConst: + const: "aaa" + default: "bbb" + WithConflictingInlineNames: + type: object + properties: + "12": + enum: ["a", "b"] + WithConflictingInlineNames1: + type: object + properties: + "2": + enum: ["c", "d"] +""", + config="literal_enums: true", +) +class TestLiteralEnumInvalidSchemas: + def test_literal_enum_bad_default_value(self, generated_client): + assert_bad_schema(generated_client, "WithBadDefaultValue", "Value B is not valid") + + def test_literal_enum_bad_default_type(self, generated_client): + assert_bad_schema(generated_client, "WithBadDefaultType", "Cannot convert 123 to enum") + + def test_literal_enum_mixed_types(self, generated_client): + assert_bad_schema(generated_client, "WithMixedTypes", "Enum values must all be the same type") + + def test_literal_enum_unsupported_type(self, generated_client): + assert_bad_schema(generated_client, "WithUnsupportedType", "Unsupported enum type") + + def test_const_default_not_matching(self, generated_client): + assert_bad_schema(generated_client, "DefaultNotMatchingConst", "Invalid value for const") + + def test_conflicting_inline_literal_enum_names(self, generated_client): + assert ( + "Found conflicting enums named WithConflictingInlineNames12 with incompatible values" + in generated_client.generator_result.output + ) + + def test_literal_enum_duplicate_values(self): + # This one currently causes a full generator failure rather than a warning + result = inline_spec_should_fail( + """ +components: + schemas: + WithDuplicateValues: + enum: ["x", "x"] +""" + ) + assert "Duplicate key X in enum" in str(result.exception) diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_references.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_references.py new file mode 100644 index 000000000..edbd84734 --- /dev/null +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_references.py @@ -0,0 +1,27 @@ +from end_to_end_tests.functional_tests.helpers import with_generated_client_fixture + + +@with_generated_client_fixture( + """ +components: + schemas: + MyModel: + type: object + properties: + booleanProp: {"type": "boolean"} + stringProp: {"type": "string"} + numberProp: {"type": "number"} + intProp: {"type": "integer"} + anyObjectProp: {"$ref": "#/components/schemas/AnyObject"} + nullProp: {"type": "null"} + anyProp: {} + AnyObject: + $ref: "#/components/schemas/OtherObject" + OtherObject: + $ref: "#/components/schemas/AnyObject" + +""" +) +class TestReferenceSchemaProperties: + def test_decode_encode(self, generated_client): + assert "Circular schema references found" in generated_client.generator_result.stderr diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_spec_format.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_spec_format.py new file mode 100644 index 000000000..5826e18ea --- /dev/null +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_spec_format.py @@ -0,0 +1,87 @@ +import pytest + +from end_to_end_tests.functional_tests.helpers import ( + inline_spec_should_fail, +) + + +class TestInvalidSpecFormats: + @pytest.mark.parametrize( + ("filename_suffix", "content", "expected_error"), + ( + (".yaml", "not a valid openapi document", "Failed to parse OpenAPI document"), + (".json", "Invalid JSON", "Invalid JSON"), + (".yaml", "{", "Invalid YAML"), + ), + ids=("invalid_openapi", "invalid_json", "invalid_yaml"), + ) + def test_unparseable_file(self, filename_suffix, content, expected_error): + result = inline_spec_should_fail(content, filename_suffix=filename_suffix, add_missing_sections=False) + assert expected_error in result.output + + def test_missing_openapi_version(self): + result = inline_spec_should_fail( + """ +info: + title: My API + version: "1.0" +paths: {} +""", + add_missing_sections=False, + ) + for text in ["Failed to parse OpenAPI document", "1 validation error", "openapi"]: + assert text in result.output + + def test_missing_title(self): + result = inline_spec_should_fail( + """ +info: + version: "1.0" +openapi: "3.1.0" +paths: {} +""", + add_missing_sections=False, + ) + for text in ["Failed to parse OpenAPI document", "1 validation error", "title"]: + assert text in result.output + + def test_missing_version(self): + result = inline_spec_should_fail( + """ +info: + title: My API +openapi: "3.1.0" +paths: {} +""", + add_missing_sections=False, + ) + for text in ["Failed to parse OpenAPI document", "1 validation error", "version"]: + assert text in result.output + + def test_missing_paths(self): + result = inline_spec_should_fail( + """ +info: + title: My API + version: "1.0" +openapi: "3.1.0" +""", + add_missing_sections=False, + ) + for text in ["Failed to parse OpenAPI document", "1 validation error", "paths"]: + assert text in result.output + + def test_swagger_unsupported(self): + result = inline_spec_should_fail( + """ +swagger: "2.0" +info: + title: My API + version: "1.0" +openapi: "3.1" +paths: {} +components: {} +""", + add_missing_sections=False, + ) + assert "You may be trying to use a Swagger document; this is not supported by this project." in result.output diff --git a/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_unions.py b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_unions.py new file mode 100644 index 000000000..81fa1c53f --- /dev/null +++ b/end_to_end_tests/functional_tests/generator_failure_cases/test_invalid_unions.py @@ -0,0 +1,28 @@ +from end_to_end_tests.functional_tests.helpers import assert_bad_schema, with_generated_client_fixture + + +@with_generated_client_fixture( + """ +components: + schemas: + UnionWithInvalidReference: + anyOf: + - $ref: "#/components/schemas/DoesntExist" + UnionWithInvalidDefault: + type: ["number", "integer"] + default: aaa + UnionWithMalformedVariant: + anyOf: + - type: string + - type: array # invalid because no items +""" +) +class TestUnionInvalidSchemas: + def test_invalid_reference(self, generated_client): + assert_bad_schema(generated_client, "UnionWithInvalidReference", "Could not find reference") + + def test_invalid_default(self, generated_client): + assert_bad_schema(generated_client, "UnionWithInvalidDefault", "Invalid int value: aaa") + + def test_invalid_property(self, generated_client): + assert_bad_schema(generated_client, "UnionWithMalformedVariant", "Invalid property in union") diff --git a/end_to_end_tests/functional_tests/helpers.py b/end_to_end_tests/functional_tests/helpers.py new file mode 100644 index 000000000..c0bc2954b --- /dev/null +++ b/end_to_end_tests/functional_tests/helpers.py @@ -0,0 +1,145 @@ +import re +from typing import Any + +import pytest +from typer.testing import Result + +from end_to_end_tests.generated_client import GeneratedClientContext, generate_client_from_inline_spec + + +def with_generated_client_fixture( + openapi_spec: str, + name: str = "generated_client", + config: str = "", + extra_args=None, +): + """Decorator to apply to a test class to create a fixture inside it called 'generated_client'. + + The fixture value will be a GeneratedClientContext created by calling + generate_client_from_inline_spec(). + """ + + if extra_args is None: + extra_args = [] + + def _decorator(cls): + # noinspection decorator + @classmethod + def generated_client(_cls): + with generate_client_from_inline_spec(openapi_spec, extra_args=extra_args, config=config) as g: + print(g.generator_result.stdout) # so we'll see the output if a test failed + yield g + + setattr(cls, name, pytest.fixture(scope="class")(generated_client)) + return cls + + return _decorator + + +def with_generated_code_import(import_path: str, alias: str | None = None): + """Decorator to apply to a test class to create a fixture from a generated code import. + + The 'generated_client' fixture must also be present. + + If import_path is "a.b.c", then the fixture's value is equal to "from a.b import c", and + its name is "c" unless you specify a different name with the alias parameter. + """ + parts = import_path.split(".") + module_name = ".".join(parts[0:-1]) + import_name = parts[-1] + + def _decorator(cls): + nonlocal alias + + def _func(self, generated_client): + return generated_client.import_symbol(module_name, import_name) + + alias = alias or import_name + _func.__name__ = alias + # noinspection bad-argument-type + setattr(cls, alias, pytest.fixture(scope="class")(classmethod(_func))) + return cls + + return _decorator + + +def with_generated_code_imports(*import_paths: str): + def _decorator(cls): + decorated = cls + for import_path in import_paths: + decorated = with_generated_code_import(import_path)(decorated) + return decorated + + return _decorator + + +def assert_model_decode_encode(model_class: Any, json_data: dict, expected_instance: Any) -> None: + instance = model_class.from_dict(json_data) + assert instance == expected_instance + assert instance.to_dict() == json_data + + +def assert_model_property_type_hint(model_class: Any, name: str, expected_type_hint: Any) -> None: + assert model_class.__annotations__[name] == expected_type_hint + + +def inline_spec_should_fail( + openapi_spec: str, + extra_args=None, + config: str = "", + filename_suffix: str = "", + add_missing_sections=True, +) -> Result: + """Asserts that the generator could not process the spec. + + Returns the command result, which could include stdout data or an exception. + """ + if extra_args is None: + extra_args = [] + with generate_client_from_inline_spec( + openapi_spec, + extra_args=extra_args, + config=config, + filename_suffix=filename_suffix, + add_missing_sections=add_missing_sections, + raise_on_error=False, + ) as generated_client: + assert generated_client.generator_result.exit_code != 0 + return generated_client.generator_result + + +def assert_bad_schema( + generated_client: GeneratedClientContext, + schema_name: str, + expected_message_str: str, +) -> None: + warnings = _GeneratorWarningsParser(generated_client) + assert schema_name in warnings.by_schema, ( + f"Did not find warning for schema {schema_name} in output: {warnings.output}" + ) + assert expected_message_str in warnings.by_schema[schema_name] + + +class _GeneratorWarningsParser: + output: str + by_schema: dict[str, str] + + def __init__(self, generated_client: GeneratedClientContext) -> None: + """Runs the generator, asserts that it printed warnings, and parses the warnings.""" + + assert generated_client.generator_result.exit_code == 0 + output = generated_client.generator_result.stdout + generated_client.generator_result.stderr + assert "Warning(s) encountered while generating" in output + self.by_schema = {} + self.output = output + bad_schema_regex = "Unable to (parse|process) schema /components/schemas/(\\w*)" + last_name = "" + while True: + if not (match := re.search(bad_schema_regex, output)): + break + if last_name: + self.by_schema[last_name] = output[0 : match.start()] + output = output[match.end() :] + last_name = match.group(2) + if last_name: + self.by_schema[last_name] = output diff --git a/end_to_end_tests/generated_client.py b/end_to_end_tests/generated_client.py new file mode 100644 index 000000000..f2b43c5ab --- /dev/null +++ b/end_to_end_tests/generated_client.py @@ -0,0 +1,164 @@ +import importlib +import os +import re +import shutil +import sys +import tempfile +from pathlib import Path +from typing import Any, Self + +import pytest +from attrs import define +from typer.testing import CliRunner, Result + +from openapi_python_client.cli import app + + +@define +class GeneratedClientContext: + """A context manager with helpers for tests that run against generated client code. + + On entering this context, sys.path is changed to include the root directory of the + generated code, so its modules can be imported. On exit, the original sys.path is + restored, and any modules that were loaded within the context are removed. + """ + + output_path: Path + generator_result: Result + base_module: str + monkeypatch: pytest.MonkeyPatch + old_modules: set[str] | None = None + + def __enter__(self) -> Self: + self.monkeypatch.syspath_prepend(self.output_path) + self.old_modules = set(sys.modules.keys()) + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.monkeypatch.undo() + for module_name in set(sys.modules.keys()) - self.old_modules: + del sys.modules[module_name] + shutil.rmtree(self.output_path, ignore_errors=True) + + def import_module(self, module_path: str) -> Any: + """Attempt to import a module from the generated code.""" + return importlib.import_module(f"{self.base_module}{module_path}") + + def import_symbol(self, module_path: str, name: str) -> Any: + module = self.import_module(module_path) + try: + return getattr(module, name) + except AttributeError as e: + existing = ", ".join(name for name in dir(module) if not name.startswith("_")) + raise AssertionError( + f'Couldn\'t find import "{name}" in "{self.base_module}{module_path}".\n' + f"Available imports in that module are: {existing}\n" + f"Output from generator was: {self.generator_result.stdout}" + ) from e + + +def _run_command( + command: str, + *, + extra_args: list[str] | None = None, + openapi_document: str | Path | None = None, + url: str | None = None, + config_path: Path | None = None, + raise_on_error: bool = True, +) -> Result: + """Generate a client from an OpenAPI document and return the result of the command.""" + runner = CliRunner() + if openapi_document is not None: + openapi_path = Path(__file__).parent / openapi_document + source_arg = f"--path={openapi_path}" + else: + source_arg = f"--url={url}" + config_path = config_path or (Path(__file__).parent / "config.yml") + args = [command, f"--config={config_path}", source_arg] + if extra_args: + args.extend(extra_args) + result = runner.invoke(app, args) + if result.exit_code != 0 and raise_on_error: + message = f"{result.stdout}\n{result.exception}" if result.exception else result.stdout + raise Exception(message) + return result + + +def generate_client( + *, + openapi_document: str | Path, + extra_args: list[str] | None = None, + output_path: str = "my-test-api-client", + base_module: str = "my_test_api_client", + overwrite: bool = True, + raise_on_error: bool = True, +) -> GeneratedClientContext: + """Run the generator and return a GeneratedClientContext for accessing the generated code.""" + args = [] + if extra_args is not None: + args = list(extra_args) + full_output_path = Path(__file__).parent / "tmp" / output_path + + if not overwrite: + shutil.rmtree(full_output_path, ignore_errors=True) + args.extend(["--output-path", str(full_output_path)]) + if overwrite: + args.append("--overwrite") + generator_result = _run_command( + "generate", extra_args=args, openapi_document=openapi_document, raise_on_error=raise_on_error + ) + return GeneratedClientContext( + full_output_path, + generator_result, + base_module, + pytest.MonkeyPatch(), + ) + + +def generate_client_from_inline_spec( + openapi_spec: str, + *, + extra_args: list[str] | None = None, + config: str = "", + filename_suffix: str | None = None, + base_module: str = "testapi_client", + add_missing_sections=True, + raise_on_error: bool = True, +) -> GeneratedClientContext: + """Run the generator on a temporary file created with the specified contents. + + You can also optionally tell it to create a temporary config file. + """ + if extra_args is None: + extra_args = [] + if add_missing_sections: + if not re.search("^openapi:", openapi_spec, re.MULTILINE): + openapi_spec += "\nopenapi: '3.1.0'\n" + if not re.search("^info:", openapi_spec, re.MULTILINE): + openapi_spec += "\ninfo: {'title': 'testapi', 'description': 'my test api', 'version': '0.0.1'}\n" + if not re.search("^paths:", openapi_spec, re.MULTILINE): + openapi_spec += "\npaths: {}\n" + + output_path = tempfile.mkdtemp() + file = tempfile.NamedTemporaryFile(suffix=filename_suffix, delete=False) + file.write(openapi_spec.encode("utf-8")) + file.close() + + if config: + config_file = tempfile.NamedTemporaryFile(delete=False) + config_file.write(config.encode("utf-8")) + config_file.close() + extra_args = [*extra_args, "--config", config_file.name] + + generated_client = generate_client( + openapi_document=file.name, + extra_args=extra_args, + output_path=output_path, + base_module=base_module, + raise_on_error=raise_on_error, + ) + os.unlink(file.name) + if config: + os.unlink(config_file.name) + + return generated_client diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/bodies/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/config/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/config/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/default/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/defaults/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/defaults/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/enums/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/enums/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/location/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/location/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/naming/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/parameters/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/responses/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/responses/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tag1/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/tag1/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tag2/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/tag2/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_multiple_files_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_multiple_files_tests_upload_post.py deleted file mode 100644 index 3f8edc817..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_multiple_files_tests_upload_post.py +++ /dev/null @@ -1,173 +0,0 @@ -from http import HTTPStatus -from typing import Any, Optional, Union - -import httpx - -from ... import errors -from ...client import AuthenticatedClient, Client -from ...models.http_validation_error import HTTPValidationError -from ...types import File, Response - - -def _get_kwargs( - *, - body: list[File], -) -> dict[str, Any]: - headers: dict[str, Any] = {} - - _kwargs: dict[str, Any] = { - "method": "post", - "url": "/tests/upload/multiple", - } - - _body = [] - for body_item_data in body: - body_item = body_item_data.to_tuple() - - _body.append(body_item) - - _kwargs["files"] = _body - - _kwargs["headers"] = headers - return _kwargs - - -def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Union[Any, HTTPValidationError]]: - if response.status_code == 200: - response_200 = response.json() - return response_200 - if response.status_code == 422: - response_422 = HTTPValidationError.from_dict(response.json()) - - return response_422 - if client.raise_on_unexpected_status: - raise errors.UnexpectedStatus(response.status_code, response.content) - else: - return None - - -def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Union[Any, HTTPValidationError]]: - return Response( - status_code=HTTPStatus(response.status_code), - content=response.content, - headers=response.headers, - parsed=_parse_response(client=client, response=response), - ) - - -def sync_detailed( - *, - client: Union[AuthenticatedClient, Client], - body: list[File], -) -> Response[Union[Any, HTTPValidationError]]: - """Upload multiple files - - Upload several files in the same request - - Args: - body (list[File]): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Union[Any, HTTPValidationError]] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = client.get_httpx_client().request( - **kwargs, - ) - - return _build_response(client=client, response=response) - - -def sync( - *, - client: Union[AuthenticatedClient, Client], - body: list[File], -) -> Optional[Union[Any, HTTPValidationError]]: - """Upload multiple files - - Upload several files in the same request - - Args: - body (list[File]): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Union[Any, HTTPValidationError] - """ - - return sync_detailed( - client=client, - body=body, - ).parsed - - -async def asyncio_detailed( - *, - client: Union[AuthenticatedClient, Client], - body: list[File], -) -> Response[Union[Any, HTTPValidationError]]: - """Upload multiple files - - Upload several files in the same request - - Args: - body (list[File]): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Response[Union[Any, HTTPValidationError]] - """ - - kwargs = _get_kwargs( - body=body, - ) - - response = await client.get_async_httpx_client().request(**kwargs) - - return _build_response(client=client, response=response) - - -async def asyncio( - *, - client: Union[AuthenticatedClient, Client], - body: list[File], -) -> Optional[Union[Any, HTTPValidationError]]: - """Upload multiple files - - Upload several files in the same request - - Args: - body (list[File]): - - Raises: - errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. - - Returns: - Union[Any, HTTPValidationError] - """ - - return ( - await asyncio_detailed( - client=client, - body=body, - ) - ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/true_/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/true_/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py deleted file mode 100644 index 659496eb6..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined.py +++ /dev/null @@ -1,74 +0,0 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union - -from attrs import define as _attrs_define - -from ..types import UNSET, Unset - -if TYPE_CHECKING: - from ..models.model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0 - from ..models.model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1 - - -T = TypeVar("T", bound="ModelWithUnionPropertyInlined") - - -@_attrs_define -class ModelWithUnionPropertyInlined: - """ - Attributes: - fruit (Union['ModelWithUnionPropertyInlinedFruitType0', 'ModelWithUnionPropertyInlinedFruitType1', Unset]): - """ - - fruit: Union["ModelWithUnionPropertyInlinedFruitType0", "ModelWithUnionPropertyInlinedFruitType1", Unset] = UNSET - - def to_dict(self) -> dict[str, Any]: - from ..models.model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0 - - fruit: Union[Unset, dict[str, Any]] - if isinstance(self.fruit, Unset): - fruit = UNSET - elif isinstance(self.fruit, ModelWithUnionPropertyInlinedFruitType0): - fruit = self.fruit.to_dict() - else: - fruit = self.fruit.to_dict() - - field_dict: dict[str, Any] = {} - field_dict.update({}) - if fruit is not UNSET: - field_dict["fruit"] = fruit - - return field_dict - - @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - from ..models.model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0 - from ..models.model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1 - - d = src_dict.copy() - - def _parse_fruit( - data: object, - ) -> Union["ModelWithUnionPropertyInlinedFruitType0", "ModelWithUnionPropertyInlinedFruitType1", Unset]: - if isinstance(data, Unset): - return data - try: - if not isinstance(data, dict): - raise TypeError() - fruit_type_0 = ModelWithUnionPropertyInlinedFruitType0.from_dict(data) - - return fruit_type_0 - except: # noqa: E722 - pass - if not isinstance(data, dict): - raise TypeError() - fruit_type_1 = ModelWithUnionPropertyInlinedFruitType1.from_dict(data) - - return fruit_type_1 - - fruit = _parse_fruit(d.pop("fruit", UNSET)) - - model_with_union_property_inlined = cls( - fruit=fruit, - ) - - return model_with_union_property_inlined diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py deleted file mode 100644 index b9ed58b8a..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/types.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Contains some shared types for properties""" - -from collections.abc import MutableMapping -from http import HTTPStatus -from typing import BinaryIO, Generic, Literal, Optional, TypeVar - -from attrs import define - - -class Unset: - def __bool__(self) -> Literal[False]: - return False - - -UNSET: Unset = Unset() - -FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]] - - -@define -class File: - """Contains information for file uploads""" - - payload: BinaryIO - file_name: Optional[str] = None - mime_type: Optional[str] = None - - def to_tuple(self) -> FileJsonType: - """Return a tuple representation that httpx will accept for multipart/form-data""" - return self.file_name, self.payload, self.mime_type - - -T = TypeVar("T") - - -@define -class Response(Generic[T]): - """A response from an endpoint""" - - status_code: HTTPStatus - content: bytes - headers: MutableMapping[str, str] - parsed: Optional[T] - - -__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"] diff --git a/end_to_end_tests/custom-templates-golden-record/README.md b/end_to_end_tests/golden-records/custom-templates/README.md similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/README.md rename to end_to_end_tests/golden-records/custom-templates/README.md diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/bodies/__init__.py similarity index 72% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/bodies/__init__.py index 89304dde0..5ff7fceb8 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/bodies/__init__.py +++ b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/bodies/__init__.py @@ -2,7 +2,7 @@ import types -from . import json_like, post_bodies_multiple, refs +from . import json_like, optional_body, post_bodies_multiple, refs class BodiesEndpoints: @@ -26,3 +26,10 @@ def refs(cls) -> types.ModuleType: Test request body defined via ref """ return refs + + @classmethod + def optional_body(cls) -> types.ModuleType: + """ + Test optional request body + """ + return optional_body diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/config/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/config/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/config/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/config/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/default/__init__.py similarity index 82% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/default/__init__.py index 0d7798e15..14b47083b 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py +++ b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/default/__init__.py @@ -7,6 +7,7 @@ get_models_allof, get_models_oneof_with_required_const, post_common_parameters, + post_types_unions_duplicate_types, reserved_parameters, ) @@ -31,3 +32,7 @@ def get_models_allof(cls) -> types.ModuleType: @classmethod def get_models_oneof_with_required_const(cls) -> types.ModuleType: return get_models_oneof_with_required_const + + @classmethod + def post_types_unions_duplicate_types(cls) -> types.ModuleType: + return post_types_unions_duplicate_types diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/defaults/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/defaults/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/defaults/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/defaults/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/enums/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/enums/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/enums/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/enums/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/location/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/location/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/location/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/location/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/naming/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/naming/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameter_references/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/parameter_references/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameter_references/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/parameter_references/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/parameters/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/parameters/__init__.py diff --git a/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/responses/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/responses/__init__.py new file mode 100644 index 000000000..d1361e311 --- /dev/null +++ b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/responses/__init__.py @@ -0,0 +1,56 @@ +"""Contains methods for accessing the API Endpoints""" + +import types + +from . import ( + default_status_code, + post_responses_unions_simple_before_complex, + reference_response, + status_code_patterns, + status_code_precedence, + text_response, +) + + +class ResponsesEndpoints: + @classmethod + def post_responses_unions_simple_before_complex(cls) -> types.ModuleType: + """ + Regression test for #603 + """ + return post_responses_unions_simple_before_complex + + @classmethod + def text_response(cls) -> types.ModuleType: + """ + Text Response + """ + return text_response + + @classmethod + def reference_response(cls) -> types.ModuleType: + """ + Endpoint using predefined response + """ + return reference_response + + @classmethod + def default_status_code(cls) -> types.ModuleType: + """ + Default Status Code Only + """ + return default_status_code + + @classmethod + def status_code_patterns(cls) -> types.ModuleType: + """ + Status Code Patterns + """ + return status_code_patterns + + @classmethod + def status_code_precedence(cls) -> types.ModuleType: + """ + Verify that specific status codes are always checked first, then ranges, then default + """ + return status_code_precedence diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tag1/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tag1/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tag1/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tag1/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tag2/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tag2/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tag2/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tag2/__init__.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tests/__init__.py similarity index 92% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tests/__init__.py index 1b91acc98..b9205c66b 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py +++ b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/tests/__init__.py @@ -21,7 +21,6 @@ token_with_cookie_auth_token_with_cookie_get, unsupported_content_tests_unsupported_content_get, upload_file_tests_upload_post, - upload_multiple_files_tests_upload_post, ) @@ -82,13 +81,6 @@ def upload_file_tests_upload_post(cls) -> types.ModuleType: """ return upload_file_tests_upload_post - @classmethod - def upload_multiple_files_tests_upload_post(cls) -> types.ModuleType: - """ - Upload several files in the same request - """ - return upload_multiple_files_tests_upload_post - @classmethod def json_body_tests_json_body_post(cls) -> types.ModuleType: """ @@ -155,6 +147,6 @@ def callback_test(cls) -> types.ModuleType: @classmethod def description_with_backslash(cls) -> types.ModuleType: """ - Test description with \ + Test description with \\ """ return description_with_backslash diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/true_/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/true_/__init__.py similarity index 100% rename from end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/true_/__init__.py rename to end_to_end_tests/golden-records/custom-templates/my_test_api_client/api/true_/__init__.py diff --git a/end_to_end_tests/golden-records/custom-templates/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/models/__init__.py new file mode 100644 index 000000000..611ed15f6 --- /dev/null +++ b/end_to_end_tests/golden-records/custom-templates/my_test_api_client/models/__init__.py @@ -0,0 +1,16 @@ +# Testing that we can access model-related information via Jinja variables. + +# To avoid having to update this file in the golden record every time the test specs are changed, +# we won't include all the classes in this output - we'll just look for one of them. + +# Using "alls" +# AModel + +# Using "imports" +# from .a_model import AModel + +# Using "openapi.models" +# AModel (a_model) + +# Using "openapi.enums" +# AnEnum (an_enum) diff --git a/end_to_end_tests/golden-record/.gitignore b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/.gitignore similarity index 100% rename from end_to_end_tests/golden-record/.gitignore rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/.gitignore diff --git a/end_to_end_tests/golden-record/README.md b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/README.md similarity index 87% rename from end_to_end_tests/golden-record/README.md rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/README.md index 79b20f411..d54f57684 100644 --- a/end_to_end_tests/golden-record/README.md +++ b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/README.md @@ -47,7 +47,7 @@ By default, when you're calling an HTTPS API it will attempt to verify that SSL ```python client = AuthenticatedClient( - base_url="https://internal_api.example.com", + base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl="/path/to/certificate_bundle.pem", ) @@ -56,11 +56,7 @@ client = AuthenticatedClient( You can also disable certificate validation altogether, but beware that **this is a security risk**. ```python -client = AuthenticatedClient( - base_url="https://internal_api.example.com", - token="SuperSecretToken", - verify_ssl=False -) +client = AuthenticatedClient(base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl=False) ``` Things to know: @@ -76,37 +72,40 @@ Things to know: ## Advanced customizations -There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx.Client` or `httpx.AsyncClient` (depending on your use-case): +There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx2.Client` or `httpx2.AsyncClient` (depending on your use-case): ```python from my_test_api_client import Client + def log_request(request): print(f"Request event hook: {request.method} {request.url} - Waiting for response") + def log_response(response): request = response.request print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") + client = Client( base_url="https://api.example.com", httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}}, ) -# Or get the underlying httpx client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() +# Or get the underlying httpx2 client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() ``` -You can even set the httpx client directly, but beware that this will override any existing settings (e.g., base_url): +You can even set the httpx2 client directly, but beware that this will override any existing settings (e.g., base_url): ```python -import httpx +import httpx2 from my_test_api_client import Client client = Client( base_url="https://api.example.com", ) # Note that base_url needs to be re-set, as would any shared cookies, headers, etc. -client.set_httpx_client(httpx.Client(base_url="https://api.example.com", proxies="http://localhost:8030")) +client.set_httpx_client(httpx2.Client(base_url="https://api.example.com", proxy="http://localhost:8030")) ``` ## Building / publishing this package diff --git a/end_to_end_tests/golden-record/my_test_api_client/__init__.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/__init__.py rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/__init__.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/__init__.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/api/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/__init__.py rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/api/__init__.py diff --git a/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/client.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/client.py new file mode 100644 index 000000000..8bd4a818d --- /dev/null +++ b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/client.py @@ -0,0 +1,260 @@ +import ssl +from typing import Any, Self + +import httpx2 +from attrs import define, evolve, field + + +@define +class Client: + """A class for keeping track of data related to the API + + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx2.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. + + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + """Whether or not to raise an errors.UnexpectedStatus if the API returns a status code that was not documented in the source OpenAPI document. Can also be provided as a keyword argument to the constructor.""" + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) + + def with_headers(self, headers: dict[str, str]) -> "Client": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "Client": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx2.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" + if self._client is None: + self._client = httpx2.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._async_client = httpx2.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +@define +class AuthenticatedClient: + """A Client which has been authenticated for use on secured endpoints + + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx2.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. + + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + """Whether or not to raise an errors.UnexpectedStatus if the API returns a status code that was not documented in the source OpenAPI document. Can also be provided as a keyword argument to the constructor.""" + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) + + token: str + """The token to use for authentication""" + prefix: str = "Bearer" + """The prefix to use for the Authorization header""" + auth_header_name: str = "Authorization" + """The name of the Authorization header""" + + def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx2.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" + if self._client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._client = httpx2.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._async_client = httpx2.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/end_to_end_tests/golden-record/my_test_api_client/errors.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/errors.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/errors.py rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/errors.py diff --git a/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/__init__.py new file mode 100644 index 000000000..3f5aca91f --- /dev/null +++ b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/__init__.py @@ -0,0 +1,9 @@ +"""Contains all the data models used in inputs/outputs""" + +from .model_with_description import ModelWithDescription +from .model_with_no_description import ModelWithNoDescription + +__all__ = ( + "ModelWithDescription", + "ModelWithNoDescription", +) diff --git a/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_description.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_description.py new file mode 100644 index 000000000..b0be6a72c --- /dev/null +++ b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_description.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ModelWithDescription") + + +@_attrs_define +class ModelWithDescription: + """This is a nice model.""" + + prop_with_no_desc: str | Unset = UNSET + prop_with_desc: str | Unset = UNSET + """ This is a nice property. """ + prop_with_long_desc: str | Unset = UNSET + """ It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of + foolishness, + it was the epoch of belief, it was the epoch of incredulity, it was the season of light, it was the season of + darkness, it was the spring of hope, it was the winter of despair. + """ + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + prop_with_no_desc = self.prop_with_no_desc + + prop_with_desc = self.prop_with_desc + + prop_with_long_desc = self.prop_with_long_desc + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if prop_with_no_desc is not UNSET: + field_dict["propWithNoDesc"] = prop_with_no_desc + if prop_with_desc is not UNSET: + field_dict["propWithDesc"] = prop_with_desc + if prop_with_long_desc is not UNSET: + field_dict["propWithLongDesc"] = prop_with_long_desc + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + prop_with_no_desc = d.pop("propWithNoDesc", UNSET) + + prop_with_desc = d.pop("propWithDesc", UNSET) + + prop_with_long_desc = d.pop("propWithLongDesc", UNSET) + + model_with_description = cls( + prop_with_no_desc=prop_with_no_desc, + prop_with_desc=prop_with_desc, + prop_with_long_desc=prop_with_long_desc, + ) + + model_with_description.additional_properties = d + return model_with_description + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_no_description.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_no_description.py new file mode 100644 index 000000000..0a020f657 --- /dev/null +++ b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/models/model_with_no_description.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ModelWithNoDescription") + + +@_attrs_define +class ModelWithNoDescription: + prop_with_no_desc: str | Unset = UNSET + prop_with_desc: str | Unset = UNSET + """ This is a nice property. """ + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + prop_with_no_desc = self.prop_with_no_desc + + prop_with_desc = self.prop_with_desc + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if prop_with_no_desc is not UNSET: + field_dict["propWithNoDesc"] = prop_with_no_desc + if prop_with_desc is not UNSET: + field_dict["propWithDesc"] = prop_with_desc + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + prop_with_no_desc = d.pop("propWithNoDesc", UNSET) + + prop_with_desc = d.pop("propWithDesc", UNSET) + + model_with_no_description = cls( + prop_with_no_desc=prop_with_no_desc, + prop_with_desc=prop_with_desc, + ) + + model_with_no_description.additional_properties = d + return model_with_no_description + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/py.typed b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/py.typed similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/py.typed rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/py.typed diff --git a/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/types.py new file mode 100644 index 000000000..9090a2620 --- /dev/null +++ b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/my_test_api_client/types.py @@ -0,0 +1,54 @@ +"""Contains some shared types for properties""" + +from collections.abc import Mapping, MutableMapping +from http import HTTPStatus +from typing import IO, BinaryIO, Generic, Literal, TypeVar + +from attrs import define + + +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + +# The types that `httpx2.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] + + +@define +class File: + """Contains information for file uploads""" + + payload: BinaryIO + file_name: str | None = None + mime_type: str | None = None + + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx2 will accept for multipart/form-data""" + return self.file_name, self.payload, self.mime_type + + +T = TypeVar("T") + + +@define +class Response(Generic[T]): + """A response from an endpoint""" + + status_code: HTTPStatus + content: bytes + headers: MutableMapping[str, str] + parsed: T | None + + +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/end_to_end_tests/golden-record/pyproject.toml b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/pyproject.toml similarity index 63% rename from end_to_end_tests/golden-record/pyproject.toml rename to end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/pyproject.toml index feca06dbd..0b22c3334 100644 --- a/end_to_end_tests/golden-record/pyproject.toml +++ b/end_to_end_tests/golden-records/docstrings-on-attributes-golden-record/pyproject.toml @@ -5,19 +5,17 @@ description = "A client library for accessing My Test API" authors = [] readme = "README.md" packages = [ - {include = "my_test_api_client"}, + { include = "my_test_api_client" }, ] -include = ["CHANGELOG.md", "my_test_api_client/py.typed"] - +include = ["my_test_api_client/py.typed"] [tool.poetry.dependencies] -python = "^3.9" -httpx = ">=0.20.0,<0.29.0" +python = "^3.11" +httpx2 = ">=2.13.1,<3" attrs = ">=22.2.0" -python-dateutil = "^2.8.0" [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.ruff] diff --git a/end_to_end_tests/literal-enums-golden-record/.gitignore b/end_to_end_tests/golden-records/escapes-client/.gitignore similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/.gitignore rename to end_to_end_tests/golden-records/escapes-client/.gitignore diff --git a/end_to_end_tests/golden-records/escapes-client/README.md b/end_to_end_tests/golden-records/escapes-client/README.md new file mode 100644 index 000000000..47a3f9f91 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/README.md @@ -0,0 +1,123 @@ +# escapes-client +A client library for accessing escapes" + +## Usage +First, create a client: + +```python +from escapes_client import Client + +client = Client(base_url="https://api.example.com") +``` + +If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead: + +```python +from escapes_client import AuthenticatedClient + +client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken") +``` + +Now call your endpoint and use your models: + +```python +from escapes_client.models import MyDataModel +from escapes_client.api.my_tag import get_my_data_model +from escapes_client.types import Response + +with client as client: + my_data: MyDataModel = get_my_data_model.sync(client=client) + # or if you need more info (e.g. status_code) + response: Response[MyDataModel] = get_my_data_model.sync_detailed(client=client) +``` + +Or do the same thing with an async version: + +```python +from escapes_client.models import MyDataModel +from escapes_client.api.my_tag import get_my_data_model +from escapes_client.types import Response + +async with client as client: + my_data: MyDataModel = await get_my_data_model.asyncio(client=client) + response: Response[MyDataModel] = await get_my_data_model.asyncio_detailed(client=client) +``` + +By default, when you're calling an HTTPS API it will attempt to verify that SSL is working correctly. Using certificate verification is highly recommended most of the time, but sometimes you may need to authenticate to a server (especially an internal server) using a custom certificate bundle. + +```python +client = AuthenticatedClient( + base_url="https://internal_api.example.com", + token="SuperSecretToken", + verify_ssl="/path/to/certificate_bundle.pem", +) +``` + +You can also disable certificate validation altogether, but beware that **this is a security risk**. + +```python +client = AuthenticatedClient(base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl=False) +``` + +Things to know: +1. Every path/method combo becomes a Python module with four functions: + 1. `sync`: Blocking request that returns parsed data (if successful) or `None` + 1. `sync_detailed`: Blocking request that always returns a `Request`, optionally with `parsed` set if the request was successful. + 1. `asyncio`: Like `sync` but async instead of blocking + 1. `asyncio_detailed`: Like `sync_detailed` but async instead of blocking + +1. All path/query params, and bodies become method arguments. +1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above) +1. Any endpoint which did not have a tag will be in `escapes_client.api.default` + +## Advanced customizations + +There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx2.Client` or `httpx2.AsyncClient` (depending on your use-case): + +```python +from escapes_client import Client + + +def log_request(request): + print(f"Request event hook: {request.method} {request.url} - Waiting for response") + + +def log_response(response): + request = response.request + print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") + + +client = Client( + base_url="https://api.example.com", + httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}}, +) + +# Or get the underlying httpx2 client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() +``` + +You can even set the httpx2 client directly, but beware that this will override any existing settings (e.g., base_url): + +```python +import httpx2 +from escapes_client import Client + +client = Client( + base_url="https://api.example.com", +) +# Note that base_url needs to be re-set, as would any shared cookies, headers, etc. +client.set_httpx_client(httpx2.Client(base_url="https://api.example.com", proxy="http://localhost:8030")) +``` + +## Building / publishing this package +This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics: +1. Update the metadata in pyproject.toml (e.g. authors, version) +1. If you're using a private repository, configure it with Poetry + 1. `poetry config repositories. ` + 1. `poetry config http-basic. ` +1. Publish the client with `poetry publish --build -r ` or, if for public PyPI, just `poetry publish --build` + +If you want to install this client into another project without publishing it (e.g. for development) then: +1. If that project **is using Poetry**, you can simply do `poetry add ` from that project +1. If that project is not using Poetry: + 1. Build a wheel with `poetry build -f wheel` + 1. Install that wheel from the other project `pip install ` diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/__init__.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/__init__.py new file mode 100644 index 000000000..204618f46 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/__init__.py @@ -0,0 +1,8 @@ +"""A client library for accessing escapes" """ + +from .client import AuthenticatedClient, Client + +__all__ = ( + "AuthenticatedClient", + "Client", +) diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/__init__.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/__init__.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/__init__.py rename to end_to_end_tests/golden-records/escapes-client/escapes_client/api/__init__.py diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/__init__.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/get_weird_responses.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/get_weird_responses.py new file mode 100644 index 000000000..c82bc005a --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/get_weird_responses.py @@ -0,0 +1,76 @@ +from http import HTTPStatus +from typing import Any + +import httpx2 + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/weird-responses", + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[Any]: + """ + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[Any]: + """ + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/misc_metadata_escapes.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/misc_metadata_escapes.py new file mode 100644 index 000000000..ed514a8ee --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/misc_metadata_escapes.py @@ -0,0 +1,150 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx2 + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.misc_metadata_escapes_body import MiscMetadataEscapesBody +from ...types import Response + + +def _get_kwargs( + *, + body: MiscMetadataEscapesBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/misc-metadata-escapes", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> str | None: + if response.status_code == 200: + response_200 = cast(str, response.json()) + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[str]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: MiscMetadataEscapesBody, +) -> Response[str]: + """ + Args: + body (MiscMetadataEscapesBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[str] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: MiscMetadataEscapesBody, +) -> str | None: + """ + Args: + body (MiscMetadataEscapesBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + str + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: MiscMetadataEscapesBody, +) -> Response[str]: + """ + Args: + body (MiscMetadataEscapesBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[str] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: MiscMetadataEscapesBody, +) -> str | None: + """ + Args: + body (MiscMetadataEscapesBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + str + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/non_string_example.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/non_string_example.py new file mode 100644 index 000000000..3c6a7debb --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/non_string_example.py @@ -0,0 +1,101 @@ +from http import HTTPStatus +from typing import Any + +import httpx2 + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.non_string_example_body import NonStringExampleBody +from ...types import Response + + +def _get_kwargs( + *, + body: NonStringExampleBody, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/non-string-example", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: + if response.status_code == 200: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: NonStringExampleBody, +) -> Response[Any]: + """ + Args: + body (NonStringExampleBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: NonStringExampleBody, +) -> Response[Any]: + """ + Args: + body (NonStringExampleBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/property_escapes.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/property_escapes.py new file mode 100644 index 000000000..b91017244 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/default/property_escapes.py @@ -0,0 +1,101 @@ +from http import HTTPStatus +from typing import Any + +import httpx2 + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.property_escapes_model_title_printuh_oh import PropertyEscapesModelTitlePrintuhOh +from ...types import Response + + +def _get_kwargs( + *, + body: PropertyEscapesModelTitlePrintuhOh, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/property-escapes", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: + if response.status_code == 200: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: PropertyEscapesModelTitlePrintuhOh, +) -> Response[Any]: + """ + Args: + body (PropertyEscapesModelTitlePrintuhOh): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: PropertyEscapesModelTitlePrintuhOh, +) -> Response[Any]: + """ + Args: + body (PropertyEscapesModelTitlePrintuhOh): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/__init__.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/with_braces_path.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/with_braces_path.py new file mode 100644 index 000000000..112b39a08 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/api/printtag_escape/with_braces_path.py @@ -0,0 +1,131 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx2 + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.with_braces_path_body import WithBracesPathBody +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + param: str, + *, + body: WithBracesPathBody, + printescape_param_name: str | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + params: dict[str, Any] = {} + + params["\\\"print('escape param name')"] = printescape_param_name + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": '/{param}/" + print("uh oh") + "'.format( + param=quote(str(param), safe=""), + ), + "params": params, + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = 'application/json; profile="https://example.com/escape" + print("uh oh") + "' + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: + if response.status_code == 200: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + param: str, + *, + client: AuthenticatedClient | Client, + body: WithBracesPathBody, + printescape_param_name: str | Unset = UNSET, +) -> Response[Any]: + """Summary \"\"\" + + Attempting to escape docstring \"\"\" print('uh oh') + + Args: + param (str): + printescape_param_name (str | Unset): + body (WithBracesPathBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + param=param, + body=body, + printescape_param_name=printescape_param_name, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + param: str, + *, + client: AuthenticatedClient | Client, + body: WithBracesPathBody, + printescape_param_name: str | Unset = UNSET, +) -> Response[Any]: + """Summary \"\"\" + + Attempting to escape docstring \"\"\" print('uh oh') + + Args: + param (str): + printescape_param_name (str | Unset): + body (WithBracesPathBody): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + param=param, + body=body, + printescape_param_name=printescape_param_name, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/client.py similarity index 72% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py rename to end_to_end_tests/golden-records/escapes-client/escapes_client/client.py index e80446f10..136b83d06 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/client.py @@ -1,7 +1,7 @@ import ssl -from typing import Any, Optional, Union +from typing import Any, Self -import httpx +import httpx2 from attrs import define, evolve, field @@ -9,7 +9,7 @@ class Client: """A class for keeping track of data related to the API - The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: ``base_url``: The base URL for the API, all requests are made to a relative path to this URL @@ -18,14 +18,14 @@ class Client: ``headers``: A dictionary of headers to be sent with every request ``timeout``: The maximum amount of a time a request can take. API functions will raise - httpx.TimeoutException if this is exceeded. + httpx2.TimeoutException if this is exceeded. ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, but can be set to False for testing purposes. ``follow_redirects``: Whether or not to follow redirects. Default value is False. - ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. Attributes: @@ -38,12 +38,12 @@ class Client: _base_url: str = field(alias="base_url") _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") - _client: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) def with_headers(self, headers: dict[str, str]) -> "Client": """Get a new client matching this one with additional headers""" @@ -61,26 +61,26 @@ def with_cookies(self, cookies: dict[str, str]) -> "Client": self._async_client.cookies.update(cookies) return evolve(self, cookies={**self._cookies, **cookies}) - def with_timeout(self, timeout: httpx.Timeout) -> "Client": - """Get a new client matching this one with a new timeout (in seconds)""" + def with_timeout(self, timeout: httpx2.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" if self._client is not None: self._client.timeout = timeout if self._async_client is not None: self._async_client.timeout = timeout return evolve(self, timeout=timeout) - def set_httpx_client(self, client: httpx.Client) -> "Client": - """Manually set the underlying httpx.Client + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._client = client return self - def get_httpx_client(self) -> httpx.Client: - """Get the underlying httpx.Client, constructing a new one if not previously set""" + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" if self._client is None: - self._client = httpx.Client( + self._client = httpx2.Client( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -91,27 +91,27 @@ def get_httpx_client(self) -> httpx.Client: ) return self._client - def __enter__(self) -> "Client": - """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" self.get_httpx_client().__enter__() return self def __exit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for internal httpx.Client (see httpx docs)""" + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" self.get_httpx_client().__exit__(*args, **kwargs) - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client": - """Manually the underlying httpx.AsyncClient + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._async_client = async_client return self - def get_async_httpx_client(self) -> httpx.AsyncClient: - """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" if self._async_client is None: - self._async_client = httpx.AsyncClient( + self._async_client = httpx2.AsyncClient( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -122,13 +122,13 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: ) return self._async_client - async def __aenter__(self) -> "Client": - """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" await self.get_async_httpx_client().__aenter__() return self async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) @@ -136,7 +136,7 @@ async def __aexit__(self, *args: Any, **kwargs: Any) -> None: class AuthenticatedClient: """A Client which has been authenticated for use on secured endpoints - The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: ``base_url``: The base URL for the API, all requests are made to a relative path to this URL @@ -145,14 +145,14 @@ class AuthenticatedClient: ``headers``: A dictionary of headers to be sent with every request ``timeout``: The maximum amount of a time a request can take. API functions will raise - httpx.TimeoutException if this is exceeded. + httpx2.TimeoutException if this is exceeded. ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, but can be set to False for testing purposes. ``follow_redirects``: Whether or not to follow redirects. Default value is False. - ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. Attributes: @@ -168,12 +168,12 @@ class AuthenticatedClient: _base_url: str = field(alias="base_url") _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") - _client: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) token: str prefix: str = "Bearer" @@ -195,27 +195,27 @@ def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": self._async_client.cookies.update(cookies) return evolve(self, cookies={**self._cookies, **cookies}) - def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": - """Get a new client matching this one with a new timeout (in seconds)""" + def with_timeout(self, timeout: httpx2.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" if self._client is not None: self._client.timeout = timeout if self._async_client is not None: self._async_client.timeout = timeout return evolve(self, timeout=timeout) - def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": - """Manually set the underlying httpx.Client + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._client = client return self - def get_httpx_client(self) -> httpx.Client: - """Get the underlying httpx.Client, constructing a new one if not previously set""" + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" if self._client is None: self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token - self._client = httpx.Client( + self._client = httpx2.Client( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -226,28 +226,28 @@ def get_httpx_client(self) -> httpx.Client: ) return self._client - def __enter__(self) -> "AuthenticatedClient": - """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" self.get_httpx_client().__enter__() return self def __exit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for internal httpx.Client (see httpx docs)""" + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" self.get_httpx_client().__exit__(*args, **kwargs) - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient": - """Manually the underlying httpx.AsyncClient + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._async_client = async_client return self - def get_async_httpx_client(self) -> httpx.AsyncClient: - """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" if self._async_client is None: self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token - self._async_client = httpx.AsyncClient( + self._async_client = httpx2.AsyncClient( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -258,11 +258,11 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: ) return self._async_client - async def __aenter__(self) -> "AuthenticatedClient": - """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" await self.get_async_httpx_client().__aenter__() return self async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/errors.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/errors.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/errors.py rename to end_to_end_tests/golden-records/escapes-client/escapes_client/errors.py diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/__init__.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/__init__.py new file mode 100644 index 000000000..99b7ab7de --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/__init__.py @@ -0,0 +1,19 @@ +"""Contains all the data models used in inputs/outputs""" + +from .misc_metadata_escapes_body import MiscMetadataEscapesBody +from .non_string_example_body import NonStringExampleBody +from .non_string_example_body_dict_example import NonStringExampleBodyDictExample +from .property_escapes_model_title_printuh_oh import PropertyEscapesModelTitlePrintuhOh +from .property_escapes_model_title_printuh_oh_escaped_enum import PropertyEscapesModelTitlePrintuhOhEscapedEnum +from .schema_printuh_oh import SchemaPrintuhOh +from .with_braces_path_body import WithBracesPathBody + +__all__ = ( + "MiscMetadataEscapesBody", + "NonStringExampleBody", + "NonStringExampleBodyDictExample", + "PropertyEscapesModelTitlePrintuhOh", + "PropertyEscapesModelTitlePrintuhOhEscapedEnum", + "SchemaPrintuhOh", + "WithBracesPathBody", +) diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/misc_metadata_escapes_body.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/misc_metadata_escapes_body.py new file mode 100644 index 000000000..98db7c4f7 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/misc_metadata_escapes_body.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="MiscMetadataEscapesBody") + + +@_attrs_define +class MiscMetadataEscapesBody: + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + misc_metadata_escapes_body = cls() + + misc_metadata_escapes_body.additional_properties = d + return misc_metadata_escapes_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body.py new file mode 100644 index 000000000..ad5d5d328 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.non_string_example_body_dict_example import NonStringExampleBodyDictExample + + +T = TypeVar("T", bound="NonStringExampleBody") + + +@_attrs_define +class NonStringExampleBody: + """ + Attributes: + dict_example (NonStringExampleBodyDictExample | Unset): Example: {'nested': 'dict example \"\"\" print(\\'uh + oh\\')'}. + """ + + dict_example: NonStringExampleBodyDictExample | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + dict_example: dict[str, Any] | Unset = UNSET + if not isinstance(self.dict_example, Unset): + dict_example = self.dict_example.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if dict_example is not UNSET: + field_dict["dict_example"] = dict_example + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.non_string_example_body_dict_example import NonStringExampleBodyDictExample # noqa: PLC0415 + + d = dict(src_dict) + _dict_example = d.pop("dict_example", UNSET) + dict_example: NonStringExampleBodyDictExample | Unset + if isinstance(_dict_example, Unset): + dict_example = UNSET + else: + dict_example = NonStringExampleBodyDictExample.from_dict(_dict_example) + + non_string_example_body = cls( + dict_example=dict_example, + ) + + non_string_example_body.additional_properties = d + return non_string_example_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body_dict_example.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body_dict_example.py new file mode 100644 index 000000000..819e8c591 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/non_string_example_body_dict_example.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="NonStringExampleBodyDictExample") + + +@_attrs_define +class NonStringExampleBodyDictExample: + """ + Example: + {'nested': 'dict example \"\"\" print(\\'uh oh\\')'} + + """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + non_string_example_body_dict_example = cls() + + non_string_example_body_dict_example.additional_properties = d + return non_string_example_body_dict_example + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh.py new file mode 100644 index 000000000..6e7d96a98 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Literal, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.property_escapes_model_title_printuh_oh_escaped_enum import PropertyEscapesModelTitlePrintuhOhEscapedEnum +from ..types import UNSET, Unset + +T = TypeVar("T", bound="PropertyEscapesModelTitlePrintuhOh") + + +@_attrs_define +class PropertyEscapesModelTitlePrintuhOh: + """ + Attributes: + escaped_description (str | Unset): Attempting to escape the Attributes docstring \"\"\" print('uh oh') Default: + 'default value with \\\\"quotes\\\\"'. Example: Example \"\"\" print('uh oh'). + escaped_enum (PropertyEscapesModelTitlePrintuhOhEscapedEnum | Unset): + escaped_const (Literal['\\\\" + print(\\'uh oh\\') + \\\\"'] | Unset): + """ + + escaped_description: str | Unset = 'default value with \\"quotes\\"' + escaped_enum: PropertyEscapesModelTitlePrintuhOhEscapedEnum | Unset = UNSET + escaped_const: Literal["\\\" + print('uh oh') + \\\""] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + escaped_description = self.escaped_description + + escaped_enum: str | Unset = UNSET + if not isinstance(self.escaped_enum, Unset): + escaped_enum = self.escaped_enum.value + + escaped_const = self.escaped_const + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if escaped_description is not UNSET: + field_dict["escaped_description"] = escaped_description + if escaped_enum is not UNSET: + field_dict["escaped_enum"] = escaped_enum + if escaped_const is not UNSET: + field_dict["escaped_const"] = escaped_const + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + escaped_description = d.pop("escaped_description", UNSET) + + _escaped_enum = d.pop("escaped_enum", UNSET) + escaped_enum: PropertyEscapesModelTitlePrintuhOhEscapedEnum | Unset + if isinstance(_escaped_enum, Unset): + escaped_enum = UNSET + else: + escaped_enum = PropertyEscapesModelTitlePrintuhOhEscapedEnum(_escaped_enum) + + escaped_const = cast(Literal["\\\" + print('uh oh') + \\\""] | Unset, d.pop("escaped_const", UNSET)) + if escaped_const != "\\\" + print('uh oh') + \\\"" and not isinstance(escaped_const, Unset): + raise ValueError( + f"escaped_const must match const '\\\\\" + print(\\'uh oh\\') + \\\\\"', got '{escaped_const}'" + ) + + property_escapes_model_title_printuh_oh = cls( + escaped_description=escaped_description, + escaped_enum=escaped_enum, + escaped_const=escaped_const, + ) + + property_escapes_model_title_printuh_oh.additional_properties = d + return property_escapes_model_title_printuh_oh + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh_escaped_enum.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh_escaped_enum.py new file mode 100644 index 000000000..2d1f0df00 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/property_escapes_model_title_printuh_oh_escaped_enum.py @@ -0,0 +1,9 @@ +from enum import StrEnum + + +class PropertyEscapesModelTitlePrintuhOhEscapedEnum(StrEnum): + NORMAL_VALUE = "normal value" + VALUE_PRINTUH_OH = "value\" + print('uh oh') + \"" + + def __str__(self) -> str: + return str(self.value) diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/schema_printuh_oh.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/schema_printuh_oh.py new file mode 100644 index 000000000..02caf1747 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/schema_printuh_oh.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="SchemaPrintuhOh") + + +@_attrs_define +class SchemaPrintuhOh: + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + schema_printuh_oh = cls() + + schema_printuh_oh.additional_properties = d + return schema_printuh_oh + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/models/with_braces_path_body.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/with_braces_path_body.py new file mode 100644 index 000000000..390de1497 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/models/with_braces_path_body.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="WithBracesPathBody") + + +@_attrs_define +class WithBracesPathBody: + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + with_braces_path_body = cls() + + with_braces_path_body.additional_properties = d + return with_braces_path_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/py.typed b/end_to_end_tests/golden-records/escapes-client/escapes_client/py.typed similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/py.typed rename to end_to_end_tests/golden-records/escapes-client/escapes_client/py.typed diff --git a/end_to_end_tests/golden-records/escapes-client/escapes_client/types.py b/end_to_end_tests/golden-records/escapes-client/escapes_client/types.py new file mode 100644 index 000000000..9090a2620 --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/escapes_client/types.py @@ -0,0 +1,54 @@ +"""Contains some shared types for properties""" + +from collections.abc import Mapping, MutableMapping +from http import HTTPStatus +from typing import IO, BinaryIO, Generic, Literal, TypeVar + +from attrs import define + + +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + +# The types that `httpx2.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] + + +@define +class File: + """Contains information for file uploads""" + + payload: BinaryIO + file_name: str | None = None + mime_type: str | None = None + + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx2 will accept for multipart/form-data""" + return self.file_name, self.payload, self.mime_type + + +T = TypeVar("T") + + +@define +class Response(Generic[T]): + """A response from an endpoint""" + + status_code: HTTPStatus + content: bytes + headers: MutableMapping[str, str] + parsed: T | None + + +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/end_to_end_tests/golden-records/escapes-client/pyproject.toml b/end_to_end_tests/golden-records/escapes-client/pyproject.toml new file mode 100644 index 000000000..083af94af --- /dev/null +++ b/end_to_end_tests/golden-records/escapes-client/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "escapes-client" +version = "1.2.3\"" +description = "A client library for accessing escapes\"" +authors = [] +readme = "README.md" +packages = [ + { include = "escapes_client" }, +] +include = ["escapes_client/py.typed"] + +[tool.poetry.dependencies] +python = "^3.11" +httpx2 = ">=2.13.1,<3" +attrs = ">=22.2.0" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = ["F", "I", "UP"] diff --git a/end_to_end_tests/test-3-1-golden-record/.gitignore b/end_to_end_tests/golden-records/my-enum-api-client/.gitignore similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/.gitignore rename to end_to_end_tests/golden-records/my-enum-api-client/.gitignore diff --git a/end_to_end_tests/literal-enums-golden-record/README.md b/end_to_end_tests/golden-records/my-enum-api-client/README.md similarity index 87% rename from end_to_end_tests/literal-enums-golden-record/README.md rename to end_to_end_tests/golden-records/my-enum-api-client/README.md index 2c6268349..1469f11c9 100644 --- a/end_to_end_tests/literal-enums-golden-record/README.md +++ b/end_to_end_tests/golden-records/my-enum-api-client/README.md @@ -47,7 +47,7 @@ By default, when you're calling an HTTPS API it will attempt to verify that SSL ```python client = AuthenticatedClient( - base_url="https://internal_api.example.com", + base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl="/path/to/certificate_bundle.pem", ) @@ -56,11 +56,7 @@ client = AuthenticatedClient( You can also disable certificate validation altogether, but beware that **this is a security risk**. ```python -client = AuthenticatedClient( - base_url="https://internal_api.example.com", - token="SuperSecretToken", - verify_ssl=False -) +client = AuthenticatedClient(base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl=False) ``` Things to know: @@ -76,37 +72,40 @@ Things to know: ## Advanced customizations -There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx.Client` or `httpx.AsyncClient` (depending on your use-case): +There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx2.Client` or `httpx2.AsyncClient` (depending on your use-case): ```python from my_enum_api_client import Client + def log_request(request): print(f"Request event hook: {request.method} {request.url} - Waiting for response") + def log_response(response): request = response.request print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") + client = Client( base_url="https://api.example.com", httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}}, ) -# Or get the underlying httpx client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() +# Or get the underlying httpx2 client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() ``` -You can even set the httpx client directly, but beware that this will override any existing settings (e.g., base_url): +You can even set the httpx2 client directly, but beware that this will override any existing settings (e.g., base_url): ```python -import httpx +import httpx2 from my_enum_api_client import Client client = Client( base_url="https://api.example.com", ) # Note that base_url needs to be re-set, as would any shared cookies, headers, etc. -client.set_httpx_client(httpx.Client(base_url="https://api.example.com", proxies="http://localhost:8030")) +client.set_httpx_client(httpx2.Client(base_url="https://api.example.com", proxy="http://localhost:8030")) ``` ## Building / publishing this package diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/__init__.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/__init__.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/__init__.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/__init__.py diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/__init__.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/__init__.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/__init__.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/__init__.py diff --git a/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/__init__.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/bool_enum_tests_bool_enum_post.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/bool_enum_tests_bool_enum_post.py similarity index 78% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/bool_enum_tests_bool_enum_post.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/bool_enum_tests_bool_enum_post.py index 52385855c..b1d18c7a7 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/bool_enum_tests_bool_enum_post.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/bool_enum_tests_bool_enum_post.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -12,6 +12,7 @@ def _get_kwargs( *, bool_enum: bool, ) -> dict[str, Any]: + params: dict[str, Any] = {} params["bool_enum"] = bool_enum @@ -27,16 +28,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -47,7 +49,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, bool_enum: bool, ) -> Response[Any]: """Bool Enum @@ -57,7 +59,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -76,7 +78,7 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, bool_enum: bool, ) -> Response[Any]: """Bool Enum @@ -86,7 +88,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/int_enum_tests_int_enum_post.py similarity index 79% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/int_enum_tests_int_enum_post.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/int_enum_tests_int_enum_post.py index af4c4ca22..a61994293 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/int_enum_tests_int_enum_post.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/enums/int_enum_tests_int_enum_post.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -13,6 +13,7 @@ def _get_kwargs( *, int_enum: AnIntEnum, ) -> dict[str, Any]: + params: dict[str, Any] = {} json_int_enum: int = int_enum @@ -29,16 +30,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -49,7 +51,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, int_enum: AnIntEnum, ) -> Response[Any]: """Int Enum @@ -59,7 +61,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -78,7 +80,7 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, int_enum: AnIntEnum, ) -> Response[Any]: """Int Enum @@ -88,7 +90,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/__init__.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/get_user_list.py similarity index 66% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/get_user_list.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/get_user_list.py index 00bc801d9..71a071755 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/get_user_list.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -18,10 +18,10 @@ def _get_kwargs( *, an_enum_value: list[AnEnum], - an_enum_value_with_null: list[Union[AnEnumWithNull, None]], + an_enum_value_with_null: list[AnEnumWithNull | None], an_enum_value_with_only_null: list[None], - int_enum_header: Union[Unset, GetUserListIntEnumHeader] = UNSET, - string_enum_header: Union[Unset, GetUserListStringEnumHeader] = UNSET, + int_enum_header: GetUserListIntEnumHeader | Unset = UNSET, + string_enum_header: GetUserListStringEnumHeader | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} if not isinstance(int_enum_header, Unset): @@ -41,7 +41,7 @@ def _get_kwargs( json_an_enum_value_with_null = [] for an_enum_value_with_null_item_data in an_enum_value_with_null: - an_enum_value_with_null_item: Union[None, str] + an_enum_value_with_null_item: None | str if isinstance(an_enum_value_with_null_item_data, str): an_enum_value_with_null_item = an_enum_value_with_null_item_data else: @@ -66,9 +66,7 @@ def _get_kwargs( return _kwargs -def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[list["AModel"]]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> list[AModel] | None: if response.status_code == 200: response_200 = [] _response_200 = response.json() @@ -78,15 +76,14 @@ def _parse_response( response_200.append(response_200_item) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[list["AModel"]]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[list[AModel]]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -97,30 +94,30 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, an_enum_value: list[AnEnum], - an_enum_value_with_null: list[Union[AnEnumWithNull, None]], + an_enum_value_with_null: list[AnEnumWithNull | None], an_enum_value_with_only_null: list[None], - int_enum_header: Union[Unset, GetUserListIntEnumHeader] = UNSET, - string_enum_header: Union[Unset, GetUserListStringEnumHeader] = UNSET, -) -> Response[list["AModel"]]: + int_enum_header: GetUserListIntEnumHeader | Unset = UNSET, + string_enum_header: GetUserListStringEnumHeader | Unset = UNSET, +) -> Response[list[AModel]]: """Get List Get a list of things Args: an_enum_value (list[AnEnum]): - an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): + an_enum_value_with_null (list[AnEnumWithNull | None]): an_enum_value_with_only_null (list[None]): - int_enum_header (Union[Unset, GetUserListIntEnumHeader]): - string_enum_header (Union[Unset, GetUserListStringEnumHeader]): + int_enum_header (GetUserListIntEnumHeader | Unset): + string_enum_header (GetUserListStringEnumHeader | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[list['AModel']] + Response[list[AModel]] """ kwargs = _get_kwargs( @@ -140,30 +137,30 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, an_enum_value: list[AnEnum], - an_enum_value_with_null: list[Union[AnEnumWithNull, None]], + an_enum_value_with_null: list[AnEnumWithNull | None], an_enum_value_with_only_null: list[None], - int_enum_header: Union[Unset, GetUserListIntEnumHeader] = UNSET, - string_enum_header: Union[Unset, GetUserListStringEnumHeader] = UNSET, -) -> Optional[list["AModel"]]: + int_enum_header: GetUserListIntEnumHeader | Unset = UNSET, + string_enum_header: GetUserListStringEnumHeader | Unset = UNSET, +) -> list[AModel] | None: """Get List Get a list of things Args: an_enum_value (list[AnEnum]): - an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): + an_enum_value_with_null (list[AnEnumWithNull | None]): an_enum_value_with_only_null (list[None]): - int_enum_header (Union[Unset, GetUserListIntEnumHeader]): - string_enum_header (Union[Unset, GetUserListStringEnumHeader]): + int_enum_header (GetUserListIntEnumHeader | Unset): + string_enum_header (GetUserListStringEnumHeader | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - list['AModel'] + list[AModel] """ return sync_detailed( @@ -178,30 +175,30 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, an_enum_value: list[AnEnum], - an_enum_value_with_null: list[Union[AnEnumWithNull, None]], + an_enum_value_with_null: list[AnEnumWithNull | None], an_enum_value_with_only_null: list[None], - int_enum_header: Union[Unset, GetUserListIntEnumHeader] = UNSET, - string_enum_header: Union[Unset, GetUserListStringEnumHeader] = UNSET, -) -> Response[list["AModel"]]: + int_enum_header: GetUserListIntEnumHeader | Unset = UNSET, + string_enum_header: GetUserListStringEnumHeader | Unset = UNSET, +) -> Response[list[AModel]]: """Get List Get a list of things Args: an_enum_value (list[AnEnum]): - an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): + an_enum_value_with_null (list[AnEnumWithNull | None]): an_enum_value_with_only_null (list[None]): - int_enum_header (Union[Unset, GetUserListIntEnumHeader]): - string_enum_header (Union[Unset, GetUserListStringEnumHeader]): + int_enum_header (GetUserListIntEnumHeader | Unset): + string_enum_header (GetUserListStringEnumHeader | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[list['AModel']] + Response[list[AModel]] """ kwargs = _get_kwargs( @@ -219,30 +216,30 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, an_enum_value: list[AnEnum], - an_enum_value_with_null: list[Union[AnEnumWithNull, None]], + an_enum_value_with_null: list[AnEnumWithNull | None], an_enum_value_with_only_null: list[None], - int_enum_header: Union[Unset, GetUserListIntEnumHeader] = UNSET, - string_enum_header: Union[Unset, GetUserListStringEnumHeader] = UNSET, -) -> Optional[list["AModel"]]: + int_enum_header: GetUserListIntEnumHeader | Unset = UNSET, + string_enum_header: GetUserListStringEnumHeader | Unset = UNSET, +) -> list[AModel] | None: """Get List Get a list of things Args: an_enum_value (list[AnEnum]): - an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): + an_enum_value_with_null (list[AnEnumWithNull | None]): an_enum_value_with_only_null (list[None]): - int_enum_header (Union[Unset, GetUserListIntEnumHeader]): - string_enum_header (Union[Unset, GetUserListStringEnumHeader]): + int_enum_header (GetUserListIntEnumHeader | Unset): + string_enum_header (GetUserListStringEnumHeader | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - list['AModel'] + list[AModel] """ return ( diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/post_user_list.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/post_user_list.py similarity index 64% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/post_user_list.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/post_user_list.py index 82df1a8f8..f1487b831 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/post_user_list.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/api/tests/post_user_list.py @@ -1,18 +1,18 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client from ...models.a_model import AModel from ...models.post_user_list_body import PostUserListBody -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, - body: PostUserListBody, + body: PostUserListBody | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -21,17 +21,14 @@ def _get_kwargs( "url": "/tests/", } - _body = body.to_multipart() - - _kwargs["files"] = _body + if not isinstance(body, Unset): + _kwargs["files"] = body.to_multipart() _kwargs["headers"] = headers return _kwargs -def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[list["AModel"]]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> list[AModel] | None: if response.status_code == 200: response_200 = [] _response_200 = response.json() @@ -41,15 +38,14 @@ def _parse_response( response_200.append(response_200_item) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[list["AModel"]]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[list[AModel]]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -60,22 +56,22 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], - body: PostUserListBody, -) -> Response[list["AModel"]]: + client: AuthenticatedClient | Client, + body: PostUserListBody | Unset = UNSET, +) -> Response[list[AModel]]: """Post List Post a list of things Args: - body (PostUserListBody): + body (PostUserListBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[list['AModel']] + Response[list[AModel]] """ kwargs = _get_kwargs( @@ -91,22 +87,22 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], - body: PostUserListBody, -) -> Optional[list["AModel"]]: + client: AuthenticatedClient | Client, + body: PostUserListBody | Unset = UNSET, +) -> list[AModel] | None: """Post List Post a list of things Args: - body (PostUserListBody): + body (PostUserListBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - list['AModel'] + list[AModel] """ return sync_detailed( @@ -117,22 +113,22 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], - body: PostUserListBody, -) -> Response[list["AModel"]]: + client: AuthenticatedClient | Client, + body: PostUserListBody | Unset = UNSET, +) -> Response[list[AModel]]: """Post List Post a list of things Args: - body (PostUserListBody): + body (PostUserListBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[list['AModel']] + Response[list[AModel]] """ kwargs = _get_kwargs( @@ -146,22 +142,22 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], - body: PostUserListBody, -) -> Optional[list["AModel"]]: + client: AuthenticatedClient | Client, + body: PostUserListBody | Unset = UNSET, +) -> list[AModel] | None: """Post List Post a list of things Args: - body (PostUserListBody): + body (PostUserListBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - list['AModel'] + list[AModel] """ return ( diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/client.py similarity index 72% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/client.py index e80446f10..136b83d06 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/client.py @@ -1,7 +1,7 @@ import ssl -from typing import Any, Optional, Union +from typing import Any, Self -import httpx +import httpx2 from attrs import define, evolve, field @@ -9,7 +9,7 @@ class Client: """A class for keeping track of data related to the API - The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: ``base_url``: The base URL for the API, all requests are made to a relative path to this URL @@ -18,14 +18,14 @@ class Client: ``headers``: A dictionary of headers to be sent with every request ``timeout``: The maximum amount of a time a request can take. API functions will raise - httpx.TimeoutException if this is exceeded. + httpx2.TimeoutException if this is exceeded. ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, but can be set to False for testing purposes. ``follow_redirects``: Whether or not to follow redirects. Default value is False. - ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. Attributes: @@ -38,12 +38,12 @@ class Client: _base_url: str = field(alias="base_url") _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") - _client: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) def with_headers(self, headers: dict[str, str]) -> "Client": """Get a new client matching this one with additional headers""" @@ -61,26 +61,26 @@ def with_cookies(self, cookies: dict[str, str]) -> "Client": self._async_client.cookies.update(cookies) return evolve(self, cookies={**self._cookies, **cookies}) - def with_timeout(self, timeout: httpx.Timeout) -> "Client": - """Get a new client matching this one with a new timeout (in seconds)""" + def with_timeout(self, timeout: httpx2.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" if self._client is not None: self._client.timeout = timeout if self._async_client is not None: self._async_client.timeout = timeout return evolve(self, timeout=timeout) - def set_httpx_client(self, client: httpx.Client) -> "Client": - """Manually set the underlying httpx.Client + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._client = client return self - def get_httpx_client(self) -> httpx.Client: - """Get the underlying httpx.Client, constructing a new one if not previously set""" + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" if self._client is None: - self._client = httpx.Client( + self._client = httpx2.Client( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -91,27 +91,27 @@ def get_httpx_client(self) -> httpx.Client: ) return self._client - def __enter__(self) -> "Client": - """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" self.get_httpx_client().__enter__() return self def __exit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for internal httpx.Client (see httpx docs)""" + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" self.get_httpx_client().__exit__(*args, **kwargs) - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client": - """Manually the underlying httpx.AsyncClient + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._async_client = async_client return self - def get_async_httpx_client(self) -> httpx.AsyncClient: - """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" if self._async_client is None: - self._async_client = httpx.AsyncClient( + self._async_client = httpx2.AsyncClient( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -122,13 +122,13 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: ) return self._async_client - async def __aenter__(self) -> "Client": - """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" await self.get_async_httpx_client().__aenter__() return self async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) @@ -136,7 +136,7 @@ async def __aexit__(self, *args: Any, **kwargs: Any) -> None: class AuthenticatedClient: """A Client which has been authenticated for use on secured endpoints - The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: ``base_url``: The base URL for the API, all requests are made to a relative path to this URL @@ -145,14 +145,14 @@ class AuthenticatedClient: ``headers``: A dictionary of headers to be sent with every request ``timeout``: The maximum amount of a time a request can take. API functions will raise - httpx.TimeoutException if this is exceeded. + httpx2.TimeoutException if this is exceeded. ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, but can be set to False for testing purposes. ``follow_redirects``: Whether or not to follow redirects. Default value is False. - ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. Attributes: @@ -168,12 +168,12 @@ class AuthenticatedClient: _base_url: str = field(alias="base_url") _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") - _client: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) token: str prefix: str = "Bearer" @@ -195,27 +195,27 @@ def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": self._async_client.cookies.update(cookies) return evolve(self, cookies={**self._cookies, **cookies}) - def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": - """Get a new client matching this one with a new timeout (in seconds)""" + def with_timeout(self, timeout: httpx2.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" if self._client is not None: self._client.timeout = timeout if self._async_client is not None: self._async_client.timeout = timeout return evolve(self, timeout=timeout) - def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": - """Manually set the underlying httpx.Client + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._client = client return self - def get_httpx_client(self) -> httpx.Client: - """Get the underlying httpx.Client, constructing a new one if not previously set""" + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" if self._client is None: self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token - self._client = httpx.Client( + self._client = httpx2.Client( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -226,28 +226,28 @@ def get_httpx_client(self) -> httpx.Client: ) return self._client - def __enter__(self) -> "AuthenticatedClient": - """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" self.get_httpx_client().__enter__() return self def __exit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for internal httpx.Client (see httpx docs)""" + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" self.get_httpx_client().__exit__(*args, **kwargs) - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient": - """Manually the underlying httpx.AsyncClient + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._async_client = async_client return self - def get_async_httpx_client(self) -> httpx.AsyncClient: - """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" if self._async_client is None: self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token - self._async_client = httpx.AsyncClient( + self._async_client = httpx2.AsyncClient( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -258,11 +258,11 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: ) return self._async_client - async def __aenter__(self) -> "AuthenticatedClient": - """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" await self.get_async_httpx_client().__aenter__() return self async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/errors.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/errors.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/errors.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/errors.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/__init__.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/__init__.py similarity index 100% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/__init__.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/__init__.py diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/a_model.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/a_model.py similarity index 69% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/a_model.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/a_model.py index 7050c1a3c..890f7507f 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/a_model.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/a_model.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define @@ -17,16 +20,16 @@ class AModel: Attributes: an_enum_value (AnEnum): For testing Enums in all the ways they can be used an_allof_enum_with_overridden_default (AnAllOfEnum): Default: 'overridden_default'. - any_value (Union[Unset, Any]): - an_optional_allof_enum (Union[Unset, AnAllOfEnum]): - nested_list_of_enums (Union[Unset, list[list[DifferentEnum]]]): + any_value (Any | Unset): + an_optional_allof_enum (AnAllOfEnum | Unset): + nested_list_of_enums (list[list[DifferentEnum]] | Unset): """ an_enum_value: AnEnum an_allof_enum_with_overridden_default: AnAllOfEnum = "overridden_default" - any_value: Union[Unset, Any] = UNSET - an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET - nested_list_of_enums: Union[Unset, list[list[DifferentEnum]]] = UNSET + any_value: Any | Unset = UNSET + an_optional_allof_enum: AnAllOfEnum | Unset = UNSET + nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET def to_dict(self) -> dict[str, Any]: an_enum_value: str = self.an_enum_value @@ -35,11 +38,11 @@ def to_dict(self) -> dict[str, Any]: any_value = self.any_value - an_optional_allof_enum: Union[Unset, str] = UNSET + an_optional_allof_enum: str | Unset = UNSET if not isinstance(self.an_optional_allof_enum, Unset): an_optional_allof_enum = self.an_optional_allof_enum - nested_list_of_enums: Union[Unset, list[list[str]]] = UNSET + nested_list_of_enums: list[list[str]] | Unset = UNSET if not isinstance(self.nested_list_of_enums, Unset): nested_list_of_enums = [] for nested_list_of_enums_item_data in self.nested_list_of_enums: @@ -51,6 +54,7 @@ def to_dict(self) -> dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) field_dict: dict[str, Any] = {} + field_dict.update( { "an_enum_value": an_enum_value, @@ -67,8 +71,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) an_enum_value = check_an_enum(d.pop("an_enum_value")) an_allof_enum_with_overridden_default = check_an_all_of_enum(d.pop("an_allof_enum_with_overridden_default")) @@ -76,23 +80,25 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: any_value = d.pop("any_value", UNSET) _an_optional_allof_enum = d.pop("an_optional_allof_enum", UNSET) - an_optional_allof_enum: Union[Unset, AnAllOfEnum] + an_optional_allof_enum: AnAllOfEnum | Unset if isinstance(_an_optional_allof_enum, Unset): an_optional_allof_enum = UNSET else: an_optional_allof_enum = check_an_all_of_enum(_an_optional_allof_enum) - nested_list_of_enums = [] _nested_list_of_enums = d.pop("nested_list_of_enums", UNSET) - for nested_list_of_enums_item_data in _nested_list_of_enums or []: - nested_list_of_enums_item = [] - _nested_list_of_enums_item = nested_list_of_enums_item_data - for nested_list_of_enums_item_item_data in _nested_list_of_enums_item: - nested_list_of_enums_item_item = check_different_enum(nested_list_of_enums_item_item_data) + nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET + if _nested_list_of_enums is not UNSET: + nested_list_of_enums = [] + for nested_list_of_enums_item_data in _nested_list_of_enums: + nested_list_of_enums_item = [] + _nested_list_of_enums_item = nested_list_of_enums_item_data + for nested_list_of_enums_item_item_data in _nested_list_of_enums_item: + nested_list_of_enums_item_item = check_different_enum(nested_list_of_enums_item_item_data) - nested_list_of_enums_item.append(nested_list_of_enums_item_item) + nested_list_of_enums_item.append(nested_list_of_enums_item_item) - nested_list_of_enums.append(nested_list_of_enums_item) + nested_list_of_enums.append(nested_list_of_enums_item) a_model = cls( an_enum_value=an_enum_value, diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_all_of_enum.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_all_of_enum.py similarity index 83% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_all_of_enum.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_all_of_enum.py index 3455e04d0..87efa8651 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_all_of_enum.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_all_of_enum.py @@ -1,4 +1,4 @@ -from typing import Literal, cast +from typing import Literal AnAllOfEnum = Literal["a_default", "bar", "foo", "overridden_default"] @@ -12,5 +12,5 @@ def check_an_all_of_enum(value: str) -> AnAllOfEnum: if value in AN_ALL_OF_ENUM_VALUES: - return cast(AnAllOfEnum, value) + return value raise TypeError(f"Unexpected value {value!r}. Expected one of {AN_ALL_OF_ENUM_VALUES!r}") diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_enum.py similarity index 80% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_enum.py index 27b5c45f9..1438bc95a 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_enum.py @@ -1,4 +1,4 @@ -from typing import Literal, cast +from typing import Literal AnEnum = Literal["FIRST_VALUE", "SECOND_VALUE"] @@ -10,5 +10,5 @@ def check_an_enum(value: str) -> AnEnum: if value in AN_ENUM_VALUES: - return cast(AnEnum, value) + return value raise TypeError(f"Unexpected value {value!r}. Expected one of {AN_ENUM_VALUES!r}") diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum_with_null.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_enum_with_null.py similarity index 82% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum_with_null.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_enum_with_null.py index 4203876de..0592fa01a 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_enum_with_null.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_enum_with_null.py @@ -1,4 +1,4 @@ -from typing import Literal, cast +from typing import Literal AnEnumWithNull = Literal["FIRST_VALUE", "SECOND_VALUE"] @@ -10,5 +10,5 @@ def check_an_enum_with_null(value: str) -> AnEnumWithNull: if value in AN_ENUM_WITH_NULL_VALUES: - return cast(AnEnumWithNull, value) + return value raise TypeError(f"Unexpected value {value!r}. Expected one of {AN_ENUM_WITH_NULL_VALUES!r}") diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_int_enum.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_int_enum.py similarity index 79% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_int_enum.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_int_enum.py index 9d0abd942..1ea0e852d 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/an_int_enum.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/an_int_enum.py @@ -1,4 +1,4 @@ -from typing import Literal, cast +from typing import Literal AnIntEnum = Literal[-1, 1, 2] @@ -11,5 +11,5 @@ def check_an_int_enum(value: int) -> AnIntEnum: if value in AN_INT_ENUM_VALUES: - return cast(AnIntEnum, value) + return value raise TypeError(f"Unexpected value {value!r}. Expected one of {AN_INT_ENUM_VALUES!r}") diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/different_enum.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/different_enum.py similarity index 80% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/different_enum.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/different_enum.py index e672a9821..086b9f490 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/different_enum.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/different_enum.py @@ -1,4 +1,4 @@ -from typing import Literal, cast +from typing import Literal DifferentEnum = Literal["DIFFERENT", "OTHER"] @@ -10,5 +10,5 @@ def check_different_enum(value: str) -> DifferentEnum: if value in DIFFERENT_ENUM_VALUES: - return cast(DifferentEnum, value) + return value raise TypeError(f"Unexpected value {value!r}. Expected one of {DIFFERENT_ENUM_VALUES!r}") diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_int_enum_header.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/get_user_list_int_enum_header.py similarity index 81% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_int_enum_header.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/get_user_list_int_enum_header.py index 845d6c2a0..9dd4e6d02 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_int_enum_header.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/get_user_list_int_enum_header.py @@ -1,4 +1,4 @@ -from typing import Literal, cast +from typing import Literal GetUserListIntEnumHeader = Literal[1, 2, 3] @@ -11,5 +11,5 @@ def check_get_user_list_int_enum_header(value: int) -> GetUserListIntEnumHeader: if value in GET_USER_LIST_INT_ENUM_HEADER_VALUES: - return cast(GetUserListIntEnumHeader, value) + return value raise TypeError(f"Unexpected value {value!r}. Expected one of {GET_USER_LIST_INT_ENUM_HEADER_VALUES!r}") diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_string_enum_header.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/get_user_list_string_enum_header.py similarity index 83% rename from end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_string_enum_header.py rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/get_user_list_string_enum_header.py index 55dbbad62..bfe72725c 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/get_user_list_string_enum_header.py +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/get_user_list_string_enum_header.py @@ -1,4 +1,4 @@ -from typing import Literal, cast +from typing import Literal GetUserListStringEnumHeader = Literal["one", "three", "two"] @@ -11,5 +11,5 @@ def check_get_user_list_string_enum_header(value: str) -> GetUserListStringEnumHeader: if value in GET_USER_LIST_STRING_ENUM_HEADER_VALUES: - return cast(GetUserListStringEnumHeader, value) + return value raise TypeError(f"Unexpected value {value!r}. Expected one of {GET_USER_LIST_STRING_ENUM_HEADER_VALUES!r}") diff --git a/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/post_user_list_body.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/post_user_list_body.py new file mode 100644 index 000000000..7e8e93950 --- /dev/null +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/models/post_user_list_body.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from .. import types +from ..models.an_all_of_enum import AnAllOfEnum, check_an_all_of_enum +from ..models.an_enum import AnEnum, check_an_enum +from ..models.an_enum_with_null import AnEnumWithNull, check_an_enum_with_null +from ..models.different_enum import DifferentEnum, check_different_enum +from ..types import UNSET, Unset + +T = TypeVar("T", bound="PostUserListBody") + + +@_attrs_define +class PostUserListBody: + """ + Attributes: + an_enum_value (list[AnEnum] | Unset): + an_enum_value_with_null (list[AnEnumWithNull | None] | Unset): + an_enum_value_with_only_null (list[None] | Unset): + an_allof_enum_with_overridden_default (AnAllOfEnum | Unset): Default: 'overridden_default'. + an_optional_allof_enum (AnAllOfEnum | Unset): + nested_list_of_enums (list[list[DifferentEnum]] | Unset): + """ + + an_enum_value: list[AnEnum] | Unset = UNSET + an_enum_value_with_null: list[AnEnumWithNull | None] | Unset = UNSET + an_enum_value_with_only_null: list[None] | Unset = UNSET + an_allof_enum_with_overridden_default: AnAllOfEnum | Unset = "overridden_default" + an_optional_allof_enum: AnAllOfEnum | Unset = UNSET + nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + an_enum_value: list[str] | Unset = UNSET + if not isinstance(self.an_enum_value, Unset): + an_enum_value = [] + for an_enum_value_item_data in self.an_enum_value: + an_enum_value_item: str = an_enum_value_item_data + an_enum_value.append(an_enum_value_item) + + an_enum_value_with_null: list[None | str] | Unset = UNSET + if not isinstance(self.an_enum_value_with_null, Unset): + an_enum_value_with_null = [] + for an_enum_value_with_null_item_data in self.an_enum_value_with_null: + an_enum_value_with_null_item: None | str + if isinstance(an_enum_value_with_null_item_data, str): + an_enum_value_with_null_item = an_enum_value_with_null_item_data + else: + an_enum_value_with_null_item = an_enum_value_with_null_item_data + an_enum_value_with_null.append(an_enum_value_with_null_item) + + an_enum_value_with_only_null: list[None] | Unset = UNSET + if not isinstance(self.an_enum_value_with_only_null, Unset): + an_enum_value_with_only_null = self.an_enum_value_with_only_null + + an_allof_enum_with_overridden_default: str | Unset = UNSET + if not isinstance(self.an_allof_enum_with_overridden_default, Unset): + an_allof_enum_with_overridden_default = self.an_allof_enum_with_overridden_default + + an_optional_allof_enum: str | Unset = UNSET + if not isinstance(self.an_optional_allof_enum, Unset): + an_optional_allof_enum = self.an_optional_allof_enum + + nested_list_of_enums: list[list[str]] | Unset = UNSET + if not isinstance(self.nested_list_of_enums, Unset): + nested_list_of_enums = [] + for nested_list_of_enums_item_data in self.nested_list_of_enums: + nested_list_of_enums_item = [] + for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: + nested_list_of_enums_item_item: str = nested_list_of_enums_item_item_data + nested_list_of_enums_item.append(nested_list_of_enums_item_item) + + nested_list_of_enums.append(nested_list_of_enums_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if an_enum_value is not UNSET: + field_dict["an_enum_value"] = an_enum_value + if an_enum_value_with_null is not UNSET: + field_dict["an_enum_value_with_null"] = an_enum_value_with_null + if an_enum_value_with_only_null is not UNSET: + field_dict["an_enum_value_with_only_null"] = an_enum_value_with_only_null + if an_allof_enum_with_overridden_default is not UNSET: + field_dict["an_allof_enum_with_overridden_default"] = an_allof_enum_with_overridden_default + if an_optional_allof_enum is not UNSET: + field_dict["an_optional_allof_enum"] = an_optional_allof_enum + if nested_list_of_enums is not UNSET: + field_dict["nested_list_of_enums"] = nested_list_of_enums + + return field_dict + + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] + + if not isinstance(self.an_enum_value, Unset): + for an_enum_value_item_element in self.an_enum_value: + files.append(("an_enum_value", (None, str(an_enum_value_item_element).encode(), "text/plain"))) + + if not isinstance(self.an_enum_value_with_null, Unset): + for an_enum_value_with_null_item_element in self.an_enum_value_with_null: + if an_enum_value_with_null_item_element is None: + files.append( + ( + "an_enum_value_with_null", + (None, str(an_enum_value_with_null_item_element).encode(), "text/plain"), + ) + ) + else: + files.append( + ( + "an_enum_value_with_null", + (None, str(an_enum_value_with_null_item_element).encode(), "text/plain"), + ) + ) + + if not isinstance(self.an_enum_value_with_only_null, Unset): + for an_enum_value_with_only_null_item_element in self.an_enum_value_with_only_null: + files.append( + ( + "an_enum_value_with_only_null", + (None, str(an_enum_value_with_only_null_item_element).encode(), "text/plain"), + ) + ) + + if not isinstance(self.an_allof_enum_with_overridden_default, Unset): + files.append( + ( + "an_allof_enum_with_overridden_default", + (None, str(self.an_allof_enum_with_overridden_default).encode(), "text/plain"), + ) + ) + + if not isinstance(self.an_optional_allof_enum, Unset): + files.append(("an_optional_allof_enum", (None, str(self.an_optional_allof_enum).encode(), "text/plain"))) + + if not isinstance(self.nested_list_of_enums, Unset): + for nested_list_of_enums_item_element in self.nested_list_of_enums: + for nested_list_of_enums_item_item_element in nested_list_of_enums_item_element: + files.append( + ( + "nested_list_of_enums", + (None, str(nested_list_of_enums_item_item_element).encode(), "text/plain"), + ) + ) + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + return files + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + _an_enum_value = d.pop("an_enum_value", UNSET) + an_enum_value: list[AnEnum] | Unset = UNSET + if _an_enum_value is not UNSET: + an_enum_value = [] + for an_enum_value_item_data in _an_enum_value: + an_enum_value_item = check_an_enum(an_enum_value_item_data) + + an_enum_value.append(an_enum_value_item) + + _an_enum_value_with_null = d.pop("an_enum_value_with_null", UNSET) + an_enum_value_with_null: list[AnEnumWithNull | None] | Unset = UNSET + if _an_enum_value_with_null is not UNSET: + an_enum_value_with_null = [] + for an_enum_value_with_null_item_data in _an_enum_value_with_null: + + def _parse_an_enum_value_with_null_item(data: object) -> AnEnumWithNull | None: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + componentsschemas_an_enum_with_null_type_1 = check_an_enum_with_null(data) + + return componentsschemas_an_enum_with_null_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(AnEnumWithNull | None, data) + + an_enum_value_with_null_item = _parse_an_enum_value_with_null_item(an_enum_value_with_null_item_data) + + an_enum_value_with_null.append(an_enum_value_with_null_item) + + an_enum_value_with_only_null = cast(list[None], d.pop("an_enum_value_with_only_null", UNSET)) + + _an_allof_enum_with_overridden_default = d.pop("an_allof_enum_with_overridden_default", UNSET) + an_allof_enum_with_overridden_default: AnAllOfEnum | Unset + if isinstance(_an_allof_enum_with_overridden_default, Unset): + an_allof_enum_with_overridden_default = UNSET + else: + an_allof_enum_with_overridden_default = check_an_all_of_enum(_an_allof_enum_with_overridden_default) + + _an_optional_allof_enum = d.pop("an_optional_allof_enum", UNSET) + an_optional_allof_enum: AnAllOfEnum | Unset + if isinstance(_an_optional_allof_enum, Unset): + an_optional_allof_enum = UNSET + else: + an_optional_allof_enum = check_an_all_of_enum(_an_optional_allof_enum) + + _nested_list_of_enums = d.pop("nested_list_of_enums", UNSET) + nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET + if _nested_list_of_enums is not UNSET: + nested_list_of_enums = [] + for nested_list_of_enums_item_data in _nested_list_of_enums: + nested_list_of_enums_item = [] + _nested_list_of_enums_item = nested_list_of_enums_item_data + for nested_list_of_enums_item_item_data in _nested_list_of_enums_item: + nested_list_of_enums_item_item = check_different_enum(nested_list_of_enums_item_item_data) + + nested_list_of_enums_item.append(nested_list_of_enums_item_item) + + nested_list_of_enums.append(nested_list_of_enums_item) + + post_user_list_body = cls( + an_enum_value=an_enum_value, + an_enum_value_with_null=an_enum_value_with_null, + an_enum_value_with_only_null=an_enum_value_with_only_null, + an_allof_enum_with_overridden_default=an_allof_enum_with_overridden_default, + an_optional_allof_enum=an_optional_allof_enum, + nested_list_of_enums=nested_list_of_enums, + ) + + post_user_list_body.additional_properties = d + return post_user_list_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/py.typed b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/py.typed similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/py.typed rename to end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/py.typed diff --git a/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/types.py b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/types.py new file mode 100644 index 000000000..9090a2620 --- /dev/null +++ b/end_to_end_tests/golden-records/my-enum-api-client/my_enum_api_client/types.py @@ -0,0 +1,54 @@ +"""Contains some shared types for properties""" + +from collections.abc import Mapping, MutableMapping +from http import HTTPStatus +from typing import IO, BinaryIO, Generic, Literal, TypeVar + +from attrs import define + + +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + +# The types that `httpx2.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] + + +@define +class File: + """Contains information for file uploads""" + + payload: BinaryIO + file_name: str | None = None + mime_type: str | None = None + + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx2 will accept for multipart/form-data""" + return self.file_name, self.payload, self.mime_type + + +T = TypeVar("T") + + +@define +class Response(Generic[T]): + """A response from an endpoint""" + + status_code: HTTPStatus + content: bytes + headers: MutableMapping[str, str] + parsed: T | None + + +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/end_to_end_tests/literal-enums-golden-record/pyproject.toml b/end_to_end_tests/golden-records/my-enum-api-client/pyproject.toml similarity index 63% rename from end_to_end_tests/literal-enums-golden-record/pyproject.toml rename to end_to_end_tests/golden-records/my-enum-api-client/pyproject.toml index 2c4d6b4e3..08419f2c8 100644 --- a/end_to_end_tests/literal-enums-golden-record/pyproject.toml +++ b/end_to_end_tests/golden-records/my-enum-api-client/pyproject.toml @@ -5,19 +5,17 @@ description = "A client library for accessing My Enum API" authors = [] readme = "README.md" packages = [ - {include = "my_enum_api_client"}, + { include = "my_enum_api_client" }, ] -include = ["CHANGELOG.md", "my_enum_api_client/py.typed"] - +include = ["my_enum_api_client/py.typed"] [tool.poetry.dependencies] -python = "^3.9" -httpx = ">=0.20.0,<0.29.0" +python = "^3.11" +httpx2 = ">=2.13.1,<3" attrs = ">=22.2.0" -python-dateutil = "^2.8.0" [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.ruff] diff --git a/end_to_end_tests/golden-records/my-test-api-client/.gitignore b/end_to_end_tests/golden-records/my-test-api-client/.gitignore new file mode 100644 index 000000000..79a2c3d73 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/.gitignore @@ -0,0 +1,23 @@ +__pycache__/ +build/ +dist/ +*.egg-info/ +.pytest_cache/ + +# pyenv +.python-version + +# Environments +.env +.venv + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# JetBrains +.idea/ + +/coverage.xml +/.coverage diff --git a/end_to_end_tests/golden-records/my-test-api-client/README.md b/end_to_end_tests/golden-records/my-test-api-client/README.md new file mode 100644 index 000000000..d54f57684 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/README.md @@ -0,0 +1,123 @@ +# my-test-api-client +A client library for accessing My Test API + +## Usage +First, create a client: + +```python +from my_test_api_client import Client + +client = Client(base_url="https://api.example.com") +``` + +If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead: + +```python +from my_test_api_client import AuthenticatedClient + +client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken") +``` + +Now call your endpoint and use your models: + +```python +from my_test_api_client.models import MyDataModel +from my_test_api_client.api.my_tag import get_my_data_model +from my_test_api_client.types import Response + +with client as client: + my_data: MyDataModel = get_my_data_model.sync(client=client) + # or if you need more info (e.g. status_code) + response: Response[MyDataModel] = get_my_data_model.sync_detailed(client=client) +``` + +Or do the same thing with an async version: + +```python +from my_test_api_client.models import MyDataModel +from my_test_api_client.api.my_tag import get_my_data_model +from my_test_api_client.types import Response + +async with client as client: + my_data: MyDataModel = await get_my_data_model.asyncio(client=client) + response: Response[MyDataModel] = await get_my_data_model.asyncio_detailed(client=client) +``` + +By default, when you're calling an HTTPS API it will attempt to verify that SSL is working correctly. Using certificate verification is highly recommended most of the time, but sometimes you may need to authenticate to a server (especially an internal server) using a custom certificate bundle. + +```python +client = AuthenticatedClient( + base_url="https://internal_api.example.com", + token="SuperSecretToken", + verify_ssl="/path/to/certificate_bundle.pem", +) +``` + +You can also disable certificate validation altogether, but beware that **this is a security risk**. + +```python +client = AuthenticatedClient(base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl=False) +``` + +Things to know: +1. Every path/method combo becomes a Python module with four functions: + 1. `sync`: Blocking request that returns parsed data (if successful) or `None` + 1. `sync_detailed`: Blocking request that always returns a `Request`, optionally with `parsed` set if the request was successful. + 1. `asyncio`: Like `sync` but async instead of blocking + 1. `asyncio_detailed`: Like `sync_detailed` but async instead of blocking + +1. All path/query params, and bodies become method arguments. +1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above) +1. Any endpoint which did not have a tag will be in `my_test_api_client.api.default` + +## Advanced customizations + +There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx2.Client` or `httpx2.AsyncClient` (depending on your use-case): + +```python +from my_test_api_client import Client + + +def log_request(request): + print(f"Request event hook: {request.method} {request.url} - Waiting for response") + + +def log_response(response): + request = response.request + print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") + + +client = Client( + base_url="https://api.example.com", + httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}}, +) + +# Or get the underlying httpx2 client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() +``` + +You can even set the httpx2 client directly, but beware that this will override any existing settings (e.g., base_url): + +```python +import httpx2 +from my_test_api_client import Client + +client = Client( + base_url="https://api.example.com", +) +# Note that base_url needs to be re-set, as would any shared cookies, headers, etc. +client.set_httpx_client(httpx2.Client(base_url="https://api.example.com", proxy="http://localhost:8030")) +``` + +## Building / publishing this package +This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics: +1. Update the metadata in pyproject.toml (e.g. authors, version) +1. If you're using a private repository, configure it with Poetry + 1. `poetry config repositories. ` + 1. `poetry config http-basic. ` +1. Publish the client with `poetry publish --build -r ` or, if for public PyPI, just `poetry publish --build` + +If you want to install this client into another project without publishing it (e.g. for development) then: +1. If that project **is using Poetry**, you can simply do `poetry add ` from that project +1. If that project is not using Poetry: + 1. Build a wheel with `poetry build -f wheel` + 1. Install that wheel from the other project `pip install ` diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/__init__.py new file mode 100644 index 000000000..3747245da --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/__init__.py @@ -0,0 +1,8 @@ +"""A client library for accessing My Test API""" + +from .client import AuthenticatedClient, Client + +__all__ = ( + "AuthenticatedClient", + "Client", +) diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/__init__.py new file mode 100644 index 000000000..81f9fa241 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/__init__.py @@ -0,0 +1 @@ +"""Contains methods for accessing the API""" diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/json_like.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/json_like.py similarity index 69% rename from end_to_end_tests/golden-record/my_test_api_client/api/bodies/json_like.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/json_like.py index 626bacfa6..037f29e94 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/json_like.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/json_like.py @@ -1,17 +1,17 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client from ...models.json_like_body import JsonLikeBody -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, - body: JsonLikeBody, + body: JsonLikeBody | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -20,25 +20,26 @@ def _get_kwargs( "url": "/bodies/json-like", } - _body = body.to_dict() + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/vnd+json" _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -49,17 +50,17 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], - body: JsonLikeBody, + client: AuthenticatedClient | Client, + body: JsonLikeBody | Unset = UNSET, ) -> Response[Any]: """A content type that works like json but isn't application/json Args: - body (JsonLikeBody): + body (JsonLikeBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -78,17 +79,17 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], - body: JsonLikeBody, + client: AuthenticatedClient | Client, + body: JsonLikeBody | Unset = UNSET, ) -> Response[Any]: """A content type that works like json but isn't application/json Args: - body (JsonLikeBody): + body (JsonLikeBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/optional_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/optional_body.py new file mode 100644 index 000000000..d5f2c50b9 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/optional_body.py @@ -0,0 +1,104 @@ +from http import HTTPStatus +from typing import Any + +import httpx2 + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.optional_body_body import OptionalBodyBody +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: OptionalBodyBody | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/bodies/optional", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: + if response.status_code == 200: + return None + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: OptionalBodyBody | Unset = UNSET, +) -> Response[Any]: + """Test optional request body + + Args: + body (OptionalBodyBody | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: OptionalBodyBody | Unset = UNSET, +) -> Response[Any]: + """Test optional request body + + Args: + body (OptionalBodyBody | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/post_bodies_multiple.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/post_bodies_multiple.py similarity index 57% rename from end_to_end_tests/golden-record/my_test_api_client/api/bodies/post_bodies_multiple.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/post_bodies_multiple.py index 84ec8eee0..b40aafab7 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/post_bodies_multiple.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/post_bodies_multiple.py @@ -1,24 +1,19 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client from ...models.post_bodies_multiple_data_body import PostBodiesMultipleDataBody from ...models.post_bodies_multiple_files_body import PostBodiesMultipleFilesBody from ...models.post_bodies_multiple_json_body import PostBodiesMultipleJsonBody -from ...types import File, Response +from ...types import UNSET, File, Response, Unset def _get_kwargs( *, - body: Union[ - PostBodiesMultipleJsonBody, - File, - PostBodiesMultipleDataBody, - PostBodiesMultipleFilesBody, - ], + body: PostBodiesMultipleJsonBody | File | PostBodiesMultipleDataBody | PostBodiesMultipleFilesBody | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -28,40 +23,37 @@ def _get_kwargs( } if isinstance(body, PostBodiesMultipleJsonBody): - _json_body = body.to_dict() + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _json_body headers["Content-Type"] = "application/json" if isinstance(body, File): - _content_body = body.payload - - _kwargs["content"] = _content_body + if not isinstance(body, Unset): + _kwargs["content"] = body.payload headers["Content-Type"] = "application/octet-stream" if isinstance(body, PostBodiesMultipleDataBody): - _data_body = body.to_dict() - - _kwargs["data"] = _data_body + if not isinstance(body, Unset): + _kwargs["data"] = body.to_dict() headers["Content-Type"] = "application/x-www-form-urlencoded" if isinstance(body, PostBodiesMultipleFilesBody): - _files_body = body.to_multipart() - - _kwargs["files"] = _files_body - headers["Content-Type"] = "multipart/form-data" + if not isinstance(body, Unset): + _kwargs["files"] = body.to_multipart() _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -72,25 +64,20 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], - body: Union[ - PostBodiesMultipleJsonBody, - File, - PostBodiesMultipleDataBody, - PostBodiesMultipleFilesBody, - ], + client: AuthenticatedClient | Client, + body: PostBodiesMultipleJsonBody | File | PostBodiesMultipleDataBody | PostBodiesMultipleFilesBody | Unset = UNSET, ) -> Response[Any]: """Test multiple bodies Args: - body (PostBodiesMultipleJsonBody): - body (File): - body (PostBodiesMultipleDataBody): - body (PostBodiesMultipleFilesBody): + body (PostBodiesMultipleJsonBody | Unset): + body (File | Unset): + body (PostBodiesMultipleDataBody | Unset): + body (PostBodiesMultipleFilesBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -109,25 +96,20 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], - body: Union[ - PostBodiesMultipleJsonBody, - File, - PostBodiesMultipleDataBody, - PostBodiesMultipleFilesBody, - ], + client: AuthenticatedClient | Client, + body: PostBodiesMultipleJsonBody | File | PostBodiesMultipleDataBody | PostBodiesMultipleFilesBody | Unset = UNSET, ) -> Response[Any]: """Test multiple bodies Args: - body (PostBodiesMultipleJsonBody): - body (File): - body (PostBodiesMultipleDataBody): - body (PostBodiesMultipleFilesBody): + body (PostBodiesMultipleJsonBody | Unset): + body (File | Unset): + body (PostBodiesMultipleDataBody | Unset): + body (PostBodiesMultipleFilesBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/refs.py similarity index 66% rename from end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/refs.py index a79cf178a..920b433d6 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/bodies/refs.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/bodies/refs.py @@ -1,17 +1,17 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client from ...models.a_model import AModel -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, - body: AModel, + body: AModel | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -20,25 +20,26 @@ def _get_kwargs( "url": "/bodies/refs", } - _body = body.to_dict() + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -49,17 +50,17 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], - body: AModel, + client: AuthenticatedClient | Client, + body: AModel | Unset = UNSET, ) -> Response[Any]: """Test request body defined via ref Args: - body (AModel): A Model for testing all the ways custom objects can be used + body (AModel | Unset): A Model for testing all the ways custom objects can be used Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -78,17 +79,17 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], - body: AModel, + client: AuthenticatedClient | Client, + body: AModel | Unset = UNSET, ) -> Response[Any]: """Test request body defined via ref Args: - body (AModel): A Model for testing all the ways custom objects can be used + body (AModel | Unset): A Model for testing all the ways custom objects can be used Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/config/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/config/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/config/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/config/content_type_override.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/config/content_type_override.py similarity index 68% rename from end_to_end_tests/golden-record/my_test_api_client/api/config/content_type_override.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/config/content_type_override.py index 2bd74aac4..b0caf1f6d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/config/content_type_override.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/config/content_type_override.py @@ -1,16 +1,16 @@ from http import HTTPStatus -from typing import Any, Optional, Union, cast +from typing import Any, cast -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, - body: str, + body: str | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -19,26 +19,27 @@ def _get_kwargs( "url": "/config/content-type-override", } - _body = body + if not isinstance(body, Unset): + _kwargs["json"] = body - _kwargs["json"] = _body headers["Content-Type"] = "openapi/python/client" _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[str]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> str | None: if response.status_code == 200: response_200 = cast(str, response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[str]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[str]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -49,17 +50,17 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], - body: str, + client: AuthenticatedClient | Client, + body: str | Unset = UNSET, ) -> Response[str]: """Content Type Override Args: - body (str): + body (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[str] @@ -78,17 +79,17 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], - body: str, -) -> Optional[str]: + client: AuthenticatedClient | Client, + body: str | Unset = UNSET, +) -> str | None: """Content Type Override Args: - body (str): + body (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: str @@ -102,17 +103,17 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], - body: str, + client: AuthenticatedClient | Client, + body: str | Unset = UNSET, ) -> Response[str]: """Content Type Override Args: - body (str): + body (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[str] @@ -129,17 +130,17 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], - body: str, -) -> Optional[str]: + client: AuthenticatedClient | Client, + body: str | Unset = UNSET, +) -> str | None: """Content Type Override Args: - body (str): + body (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: str diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/get_common_parameters.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_common_parameters.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/get_common_parameters.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_common_parameters.py index 7de222f55..004570e86 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/default/get_common_parameters.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_common_parameters.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -10,8 +10,9 @@ def _get_kwargs( *, - common: Union[Unset, str] = UNSET, + common: str | Unset = UNSET, ) -> dict[str, Any]: + params: dict[str, Any] = {} params["common"] = common @@ -27,16 +28,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -47,16 +49,16 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], - common: Union[Unset, str] = UNSET, + client: AuthenticatedClient | Client, + common: str | Unset = UNSET, ) -> Response[Any]: """ Args: - common (Union[Unset, str]): + common (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -75,16 +77,16 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], - common: Union[Unset, str] = UNSET, + client: AuthenticatedClient | Client, + common: str | Unset = UNSET, ) -> Response[Any]: """ Args: - common (Union[Unset, str]): + common (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_allof.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_models_allof.py similarity index 74% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_allof.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_models_allof.py index 9d837acd6..bf3061de4 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_allof.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_models_allof.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -10,6 +10,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/models/allof", @@ -19,12 +20,13 @@ def _get_kwargs() -> dict[str, Any]: def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[GetModelsAllofResponse200]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> GetModelsAllofResponse200 | None: if response.status_code == 200: response_200 = GetModelsAllofResponse200.from_dict(response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -32,7 +34,7 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response + *, client: AuthenticatedClient | Client, response: httpx2.Response ) -> Response[GetModelsAllofResponse200]: return Response( status_code=HTTPStatus(response.status_code), @@ -44,12 +46,12 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[GetModelsAllofResponse200]: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[GetModelsAllofResponse200] @@ -66,12 +68,12 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[GetModelsAllofResponse200]: + client: AuthenticatedClient | Client, +) -> GetModelsAllofResponse200 | None: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: GetModelsAllofResponse200 @@ -84,12 +86,12 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[GetModelsAllofResponse200]: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[GetModelsAllofResponse200] @@ -104,12 +106,12 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[GetModelsAllofResponse200]: + client: AuthenticatedClient | Client, +) -> GetModelsAllofResponse200 | None: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: GetModelsAllofResponse200 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_oneof_with_required_const.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_models_oneof_with_required_const.py similarity index 57% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_oneof_with_required_const.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_models_oneof_with_required_const.py index 85f68fb7c..24e29e82b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/default/get_models_oneof_with_required_const.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/get_models_oneof_with_required_const.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -15,6 +15,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/models/oneof-with-required-const", @@ -24,24 +25,20 @@ def _get_kwargs() -> dict[str, Any]: def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[ - Union["GetModelsOneofWithRequiredConstResponse200Type0", "GetModelsOneofWithRequiredConstResponse200Type1"] -]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> GetModelsOneofWithRequiredConstResponse200Type0 | GetModelsOneofWithRequiredConstResponse200Type1 | None: if response.status_code == 200: def _parse_response_200( data: object, - ) -> Union[ - "GetModelsOneofWithRequiredConstResponse200Type0", "GetModelsOneofWithRequiredConstResponse200Type1" - ]: + ) -> GetModelsOneofWithRequiredConstResponse200Type0 | GetModelsOneofWithRequiredConstResponse200Type1: try: if not isinstance(data, dict): raise TypeError() response_200_type_0 = GetModelsOneofWithRequiredConstResponse200Type0.from_dict(data) return response_200_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass if not isinstance(data, dict): raise TypeError() @@ -52,6 +49,7 @@ def _parse_response_200( response_200 = _parse_response_200(response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -59,10 +57,8 @@ def _parse_response_200( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[ - Union["GetModelsOneofWithRequiredConstResponse200Type0", "GetModelsOneofWithRequiredConstResponse200Type1"] -]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Response[GetModelsOneofWithRequiredConstResponse200Type0 | GetModelsOneofWithRequiredConstResponse200Type1]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -73,17 +69,15 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], -) -> Response[ - Union["GetModelsOneofWithRequiredConstResponse200Type0", "GetModelsOneofWithRequiredConstResponse200Type1"] -]: + client: AuthenticatedClient | Client, +) -> Response[GetModelsOneofWithRequiredConstResponse200Type0 | GetModelsOneofWithRequiredConstResponse200Type1]: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union['GetModelsOneofWithRequiredConstResponse200Type0', 'GetModelsOneofWithRequiredConstResponse200Type1']] + Response[GetModelsOneofWithRequiredConstResponse200Type0 | GetModelsOneofWithRequiredConstResponse200Type1] """ kwargs = _get_kwargs() @@ -97,17 +91,15 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[ - Union["GetModelsOneofWithRequiredConstResponse200Type0", "GetModelsOneofWithRequiredConstResponse200Type1"] -]: + client: AuthenticatedClient | Client, +) -> GetModelsOneofWithRequiredConstResponse200Type0 | GetModelsOneofWithRequiredConstResponse200Type1 | None: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union['GetModelsOneofWithRequiredConstResponse200Type0', 'GetModelsOneofWithRequiredConstResponse200Type1'] + GetModelsOneofWithRequiredConstResponse200Type0 | GetModelsOneofWithRequiredConstResponse200Type1 """ return sync_detailed( @@ -117,17 +109,15 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], -) -> Response[ - Union["GetModelsOneofWithRequiredConstResponse200Type0", "GetModelsOneofWithRequiredConstResponse200Type1"] -]: + client: AuthenticatedClient | Client, +) -> Response[GetModelsOneofWithRequiredConstResponse200Type0 | GetModelsOneofWithRequiredConstResponse200Type1]: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union['GetModelsOneofWithRequiredConstResponse200Type0', 'GetModelsOneofWithRequiredConstResponse200Type1']] + Response[GetModelsOneofWithRequiredConstResponse200Type0 | GetModelsOneofWithRequiredConstResponse200Type1] """ kwargs = _get_kwargs() @@ -139,17 +129,15 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[ - Union["GetModelsOneofWithRequiredConstResponse200Type0", "GetModelsOneofWithRequiredConstResponse200Type1"] -]: + client: AuthenticatedClient | Client, +) -> GetModelsOneofWithRequiredConstResponse200Type0 | GetModelsOneofWithRequiredConstResponse200Type1 | None: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union['GetModelsOneofWithRequiredConstResponse200Type0', 'GetModelsOneofWithRequiredConstResponse200Type1'] + GetModelsOneofWithRequiredConstResponse200Type0 | GetModelsOneofWithRequiredConstResponse200Type1 """ return ( diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/post_common_parameters.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/post_common_parameters.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/post_common_parameters.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/post_common_parameters.py index 5bd941c69..59dffd69e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/default/post_common_parameters.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/post_common_parameters.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -10,8 +10,9 @@ def _get_kwargs( *, - common: Union[Unset, str] = UNSET, + common: str | Unset = UNSET, ) -> dict[str, Any]: + params: dict[str, Any] = {} params["common"] = common @@ -27,16 +28,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -47,16 +49,16 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], - common: Union[Unset, str] = UNSET, + client: AuthenticatedClient | Client, + common: str | Unset = UNSET, ) -> Response[Any]: """ Args: - common (Union[Unset, str]): + common (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -75,16 +77,16 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], - common: Union[Unset, str] = UNSET, + client: AuthenticatedClient | Client, + common: str | Unset = UNSET, ) -> Response[Any]: """ Args: - common (Union[Unset, str]): + common (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/post_types_unions_duplicate_types.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/post_types_unions_duplicate_types.py new file mode 100644 index 000000000..18ab41553 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/post_types_unions_duplicate_types.py @@ -0,0 +1,160 @@ +from http import HTTPStatus +from typing import Any + +import httpx2 + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.a_model import AModel +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: AModel | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/types/unions/duplicate-types", + } + + if isinstance(body, AModel): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> AModel | None: + if response.status_code == 200: + + def _parse_response_200(data: object) -> AModel: + if not isinstance(data, dict): + raise TypeError() + response_200_type_0 = AModel.from_dict(data) + + return response_200_type_0 + + response_200 = _parse_response_200(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[AModel]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: AModel | Unset = UNSET, +) -> Response[AModel]: + """ + Args: + body (AModel | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[AModel] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: AModel | Unset = UNSET, +) -> AModel | None: + """ + Args: + body (AModel | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + AModel + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: AModel | Unset = UNSET, +) -> Response[AModel]: + """ + Args: + body (AModel | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[AModel] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: AModel | Unset = UNSET, +) -> AModel | None: + """ + Args: + body (AModel | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + AModel + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/reserved_parameters.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/reserved_parameters.py similarity index 80% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/reserved_parameters.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/reserved_parameters.py index fe7adf04c..e5eb4b0bc 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/default/reserved_parameters.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/default/reserved_parameters.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -13,6 +13,7 @@ def _get_kwargs( client_query: str, url_query: str, ) -> dict[str, Any]: + params: dict[str, Any] = {} params["client"] = client_query @@ -30,16 +31,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -50,7 +52,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, client_query: str, url_query: str, ) -> Response[Any]: @@ -61,7 +63,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -81,7 +83,7 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, client_query: str, url_query: str, ) -> Response[Any]: @@ -92,7 +94,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/defaults/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/defaults/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/defaults/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/defaults/defaults_tests_defaults_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/defaults/defaults_tests_defaults_post.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/api/defaults/defaults_tests_defaults_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/defaults/defaults_tests_defaults_post.py index ffc9b535e..b38047ffa 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/defaults/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/defaults/defaults_tests_defaults_post.py @@ -1,9 +1,8 @@ import datetime from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx -from dateutil.parser import isoparse +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -17,18 +16,19 @@ def _get_kwargs( *, string_prop: str = "the default string", string_with_num: str = "1", - date_prop: datetime.date = isoparse("1010-10-10").date(), + date_prop: datetime.date = datetime.date.fromisoformat("1010-10-10"), float_prop: float = 3.14, float_with_int: float = 3.0, int_prop: int = 7, boolean_prop: bool = False, list_prop: list[AnEnum], - union_prop: Union[float, str] = "not a float", - union_prop_with_ref: Union[AnEnum, Unset, float] = 0.6, + union_prop: float | str = "not a float", + union_prop_with_ref: AnEnum | float | Unset = 0.6, enum_prop: AnEnum, - model_prop: "ModelWithUnionProperty", - required_model_prop: "ModelWithUnionProperty", + model_prop: ModelWithUnionProperty, + required_model_prop: ModelWithUnionProperty, ) -> dict[str, Any]: + params: dict[str, Any] = {} params["string_prop"] = string_prop @@ -53,11 +53,11 @@ def _get_kwargs( params["list_prop"] = json_list_prop - json_union_prop: Union[float, str] + json_union_prop: float | str json_union_prop = union_prop params["union_prop"] = json_union_prop - json_union_prop_with_ref: Union[Unset, float, str] + json_union_prop_with_ref: float | str | Unset if isinstance(union_prop_with_ref, Unset): json_union_prop_with_ref = UNSET elif isinstance(union_prop_with_ref, AnEnum): @@ -87,15 +87,17 @@ def _get_kwargs( def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Union[Any, HTTPValidationError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Any | HTTPValidationError | None: if response.status_code == 200: response_200 = response.json() return response_200 + if response.status_code == 422: response_422 = HTTPValidationError.from_dict(response.json()) return response_422 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -103,8 +105,8 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Union[Any, HTTPValidationError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Response[Any | HTTPValidationError]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -115,44 +117,44 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, string_prop: str = "the default string", string_with_num: str = "1", - date_prop: datetime.date = isoparse("1010-10-10").date(), + date_prop: datetime.date = datetime.date.fromisoformat("1010-10-10"), float_prop: float = 3.14, float_with_int: float = 3.0, int_prop: int = 7, boolean_prop: bool = False, list_prop: list[AnEnum], - union_prop: Union[float, str] = "not a float", - union_prop_with_ref: Union[AnEnum, Unset, float] = 0.6, + union_prop: float | str = "not a float", + union_prop_with_ref: AnEnum | float | Unset = 0.6, enum_prop: AnEnum, - model_prop: "ModelWithUnionProperty", - required_model_prop: "ModelWithUnionProperty", -) -> Response[Union[Any, HTTPValidationError]]: + model_prop: ModelWithUnionProperty, + required_model_prop: ModelWithUnionProperty, +) -> Response[Any | HTTPValidationError]: """Defaults Args: string_prop (str): Default: 'the default string'. string_with_num (str): Default: '1'. - date_prop (datetime.date): Default: isoparse('1010-10-10').date(). + date_prop (datetime.date): Default: datetime.date.fromisoformat('1010-10-10'). float_prop (float): Default: 3.14. float_with_int (float): Default: 3.0. int_prop (int): Default: 7. boolean_prop (bool): Default: False. list_prop (list[AnEnum]): - union_prop (Union[float, str]): Default: 'not a float'. - union_prop_with_ref (Union[AnEnum, Unset, float]): Default: 0.6. + union_prop (float | str): Default: 'not a float'. + union_prop_with_ref (AnEnum | float | Unset): Default: 0.6. enum_prop (AnEnum): For testing Enums in all the ways they can be used model_prop (ModelWithUnionProperty): required_model_prop (ModelWithUnionProperty): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[Any, HTTPValidationError]] + Response[Any | HTTPValidationError] """ kwargs = _get_kwargs( @@ -180,44 +182,44 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, string_prop: str = "the default string", string_with_num: str = "1", - date_prop: datetime.date = isoparse("1010-10-10").date(), + date_prop: datetime.date = datetime.date.fromisoformat("1010-10-10"), float_prop: float = 3.14, float_with_int: float = 3.0, int_prop: int = 7, boolean_prop: bool = False, list_prop: list[AnEnum], - union_prop: Union[float, str] = "not a float", - union_prop_with_ref: Union[AnEnum, Unset, float] = 0.6, + union_prop: float | str = "not a float", + union_prop_with_ref: AnEnum | float | Unset = 0.6, enum_prop: AnEnum, - model_prop: "ModelWithUnionProperty", - required_model_prop: "ModelWithUnionProperty", -) -> Optional[Union[Any, HTTPValidationError]]: + model_prop: ModelWithUnionProperty, + required_model_prop: ModelWithUnionProperty, +) -> Any | HTTPValidationError | None: """Defaults Args: string_prop (str): Default: 'the default string'. string_with_num (str): Default: '1'. - date_prop (datetime.date): Default: isoparse('1010-10-10').date(). + date_prop (datetime.date): Default: datetime.date.fromisoformat('1010-10-10'). float_prop (float): Default: 3.14. float_with_int (float): Default: 3.0. int_prop (int): Default: 7. boolean_prop (bool): Default: False. list_prop (list[AnEnum]): - union_prop (Union[float, str]): Default: 'not a float'. - union_prop_with_ref (Union[AnEnum, Unset, float]): Default: 0.6. + union_prop (float | str): Default: 'not a float'. + union_prop_with_ref (AnEnum | float | Unset): Default: 0.6. enum_prop (AnEnum): For testing Enums in all the ways they can be used model_prop (ModelWithUnionProperty): required_model_prop (ModelWithUnionProperty): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[Any, HTTPValidationError] + Any | HTTPValidationError """ return sync_detailed( @@ -240,44 +242,44 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, string_prop: str = "the default string", string_with_num: str = "1", - date_prop: datetime.date = isoparse("1010-10-10").date(), + date_prop: datetime.date = datetime.date.fromisoformat("1010-10-10"), float_prop: float = 3.14, float_with_int: float = 3.0, int_prop: int = 7, boolean_prop: bool = False, list_prop: list[AnEnum], - union_prop: Union[float, str] = "not a float", - union_prop_with_ref: Union[AnEnum, Unset, float] = 0.6, + union_prop: float | str = "not a float", + union_prop_with_ref: AnEnum | float | Unset = 0.6, enum_prop: AnEnum, - model_prop: "ModelWithUnionProperty", - required_model_prop: "ModelWithUnionProperty", -) -> Response[Union[Any, HTTPValidationError]]: + model_prop: ModelWithUnionProperty, + required_model_prop: ModelWithUnionProperty, +) -> Response[Any | HTTPValidationError]: """Defaults Args: string_prop (str): Default: 'the default string'. string_with_num (str): Default: '1'. - date_prop (datetime.date): Default: isoparse('1010-10-10').date(). + date_prop (datetime.date): Default: datetime.date.fromisoformat('1010-10-10'). float_prop (float): Default: 3.14. float_with_int (float): Default: 3.0. int_prop (int): Default: 7. boolean_prop (bool): Default: False. list_prop (list[AnEnum]): - union_prop (Union[float, str]): Default: 'not a float'. - union_prop_with_ref (Union[AnEnum, Unset, float]): Default: 0.6. + union_prop (float | str): Default: 'not a float'. + union_prop_with_ref (AnEnum | float | Unset): Default: 0.6. enum_prop (AnEnum): For testing Enums in all the ways they can be used model_prop (ModelWithUnionProperty): required_model_prop (ModelWithUnionProperty): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[Any, HTTPValidationError]] + Response[Any | HTTPValidationError] """ kwargs = _get_kwargs( @@ -303,44 +305,44 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, string_prop: str = "the default string", string_with_num: str = "1", - date_prop: datetime.date = isoparse("1010-10-10").date(), + date_prop: datetime.date = datetime.date.fromisoformat("1010-10-10"), float_prop: float = 3.14, float_with_int: float = 3.0, int_prop: int = 7, boolean_prop: bool = False, list_prop: list[AnEnum], - union_prop: Union[float, str] = "not a float", - union_prop_with_ref: Union[AnEnum, Unset, float] = 0.6, + union_prop: float | str = "not a float", + union_prop_with_ref: AnEnum | float | Unset = 0.6, enum_prop: AnEnum, - model_prop: "ModelWithUnionProperty", - required_model_prop: "ModelWithUnionProperty", -) -> Optional[Union[Any, HTTPValidationError]]: + model_prop: ModelWithUnionProperty, + required_model_prop: ModelWithUnionProperty, +) -> Any | HTTPValidationError | None: """Defaults Args: string_prop (str): Default: 'the default string'. string_with_num (str): Default: '1'. - date_prop (datetime.date): Default: isoparse('1010-10-10').date(). + date_prop (datetime.date): Default: datetime.date.fromisoformat('1010-10-10'). float_prop (float): Default: 3.14. float_with_int (float): Default: 3.0. int_prop (int): Default: 7. boolean_prop (bool): Default: False. list_prop (list[AnEnum]): - union_prop (Union[float, str]): Default: 'not a float'. - union_prop_with_ref (Union[AnEnum, Unset, float]): Default: 0.6. + union_prop (float | str): Default: 'not a float'. + union_prop_with_ref (AnEnum | float | Unset): Default: 0.6. enum_prop (AnEnum): For testing Enums in all the ways they can be used model_prop (ModelWithUnionProperty): required_model_prop (ModelWithUnionProperty): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[Any, HTTPValidationError] + Any | HTTPValidationError """ return ( diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/enums/bool_enum_tests_bool_enum_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/bool_enum_tests_bool_enum_post.py similarity index 78% rename from end_to_end_tests/golden-record/my_test_api_client/api/enums/bool_enum_tests_bool_enum_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/bool_enum_tests_bool_enum_post.py index 52385855c..b1d18c7a7 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/enums/bool_enum_tests_bool_enum_post.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/bool_enum_tests_bool_enum_post.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -12,6 +12,7 @@ def _get_kwargs( *, bool_enum: bool, ) -> dict[str, Any]: + params: dict[str, Any] = {} params["bool_enum"] = bool_enum @@ -27,16 +28,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -47,7 +49,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, bool_enum: bool, ) -> Response[Any]: """Bool Enum @@ -57,7 +59,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -76,7 +78,7 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, bool_enum: bool, ) -> Response[Any]: """Bool Enum @@ -86,7 +88,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/enums/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/int_enum_tests_int_enum_post.py similarity index 79% rename from end_to_end_tests/golden-record/my_test_api_client/api/enums/int_enum_tests_int_enum_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/int_enum_tests_int_enum_post.py index 26c3729fe..a9c50eff3 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/enums/int_enum_tests_int_enum_post.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/enums/int_enum_tests_int_enum_post.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -13,6 +13,7 @@ def _get_kwargs( *, int_enum: AnIntEnum, ) -> dict[str, Any]: + params: dict[str, Any] = {} json_int_enum = int_enum.value @@ -29,16 +30,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -49,7 +51,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, int_enum: AnIntEnum, ) -> Response[Any]: """Int Enum @@ -59,7 +61,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -78,7 +80,7 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, int_enum: AnIntEnum, ) -> Response[Any]: """Int Enum @@ -88,7 +90,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/location/get_location_header_types.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/get_location_header_types.py similarity index 56% rename from end_to_end_tests/golden-record/my_test_api_client/api/location/get_location_header_types.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/get_location_header_types.py index ad9428a72..bd4b7d30e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/location/get_location_header_types.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/get_location_header_types.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -12,12 +12,12 @@ def _get_kwargs( *, - boolean_header: Union[Unset, bool] = UNSET, - string_header: Union[Unset, str] = UNSET, - number_header: Union[Unset, float] = UNSET, - integer_header: Union[Unset, int] = UNSET, - int_enum_header: Union[Unset, GetLocationHeaderTypesIntEnumHeader] = UNSET, - string_enum_header: Union[Unset, GetLocationHeaderTypesStringEnumHeader] = UNSET, + boolean_header: bool | Unset = UNSET, + string_header: str | Unset = UNSET, + number_header: float | Unset = UNSET, + integer_header: int | Unset = UNSET, + int_enum_header: GetLocationHeaderTypesIntEnumHeader | Unset = UNSET, + string_enum_header: GetLocationHeaderTypesStringEnumHeader | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} if not isinstance(boolean_header, Unset): @@ -47,16 +47,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -67,26 +68,26 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], - boolean_header: Union[Unset, bool] = UNSET, - string_header: Union[Unset, str] = UNSET, - number_header: Union[Unset, float] = UNSET, - integer_header: Union[Unset, int] = UNSET, - int_enum_header: Union[Unset, GetLocationHeaderTypesIntEnumHeader] = UNSET, - string_enum_header: Union[Unset, GetLocationHeaderTypesStringEnumHeader] = UNSET, + client: AuthenticatedClient | Client, + boolean_header: bool | Unset = UNSET, + string_header: str | Unset = UNSET, + number_header: float | Unset = UNSET, + integer_header: int | Unset = UNSET, + int_enum_header: GetLocationHeaderTypesIntEnumHeader | Unset = UNSET, + string_enum_header: GetLocationHeaderTypesStringEnumHeader | Unset = UNSET, ) -> Response[Any]: """ Args: - boolean_header (Union[Unset, bool]): - string_header (Union[Unset, str]): - number_header (Union[Unset, float]): - integer_header (Union[Unset, int]): - int_enum_header (Union[Unset, GetLocationHeaderTypesIntEnumHeader]): - string_enum_header (Union[Unset, GetLocationHeaderTypesStringEnumHeader]): + boolean_header (bool | Unset): + string_header (str | Unset): + number_header (float | Unset): + integer_header (int | Unset): + int_enum_header (GetLocationHeaderTypesIntEnumHeader | Unset): + string_enum_header (GetLocationHeaderTypesStringEnumHeader | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -110,26 +111,26 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], - boolean_header: Union[Unset, bool] = UNSET, - string_header: Union[Unset, str] = UNSET, - number_header: Union[Unset, float] = UNSET, - integer_header: Union[Unset, int] = UNSET, - int_enum_header: Union[Unset, GetLocationHeaderTypesIntEnumHeader] = UNSET, - string_enum_header: Union[Unset, GetLocationHeaderTypesStringEnumHeader] = UNSET, + client: AuthenticatedClient | Client, + boolean_header: bool | Unset = UNSET, + string_header: str | Unset = UNSET, + number_header: float | Unset = UNSET, + integer_header: int | Unset = UNSET, + int_enum_header: GetLocationHeaderTypesIntEnumHeader | Unset = UNSET, + string_enum_header: GetLocationHeaderTypesStringEnumHeader | Unset = UNSET, ) -> Response[Any]: """ Args: - boolean_header (Union[Unset, bool]): - string_header (Union[Unset, str]): - number_header (Union[Unset, float]): - integer_header (Union[Unset, int]): - int_enum_header (Union[Unset, GetLocationHeaderTypesIntEnumHeader]): - string_enum_header (Union[Unset, GetLocationHeaderTypesStringEnumHeader]): + boolean_header (bool | Unset): + string_header (str | Unset): + number_header (float | Unset): + integer_header (int | Unset): + int_enum_header (GetLocationHeaderTypesIntEnumHeader | Unset): + string_enum_header (GetLocationHeaderTypesStringEnumHeader | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/location/get_location_query_optionality.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/get_location_query_optionality.py similarity index 66% rename from end_to_end_tests/golden-record/my_test_api_client/api/location/get_location_query_optionality.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/get_location_query_optionality.py index e28e37a36..92ecb0126 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/location/get_location_query_optionality.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/location/get_location_query_optionality.py @@ -1,8 +1,8 @@ import datetime from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -12,23 +12,24 @@ def _get_kwargs( *, not_null_required: datetime.datetime, - null_required: Union[None, datetime.datetime], - null_not_required: Union[None, Unset, datetime.datetime] = UNSET, - not_null_not_required: Union[Unset, datetime.datetime] = UNSET, + null_required: datetime.datetime | None, + null_not_required: datetime.datetime | None | Unset = UNSET, + not_null_not_required: datetime.datetime | Unset = UNSET, ) -> dict[str, Any]: + params: dict[str, Any] = {} json_not_null_required = not_null_required.isoformat() params["not_null_required"] = json_not_null_required - json_null_required: Union[None, str] + json_null_required: None | str if isinstance(null_required, datetime.datetime): json_null_required = null_required.isoformat() else: json_null_required = null_required params["null_required"] = json_null_required - json_null_not_required: Union[None, Unset, str] + json_null_not_required: None | str | Unset if isinstance(null_not_required, Unset): json_null_not_required = UNSET elif isinstance(null_not_required, datetime.datetime): @@ -37,7 +38,7 @@ def _get_kwargs( json_null_not_required = null_not_required params["null_not_required"] = json_null_not_required - json_not_null_not_required: Union[Unset, str] = UNSET + json_not_null_not_required: str | Unset = UNSET if not isinstance(not_null_not_required, Unset): json_not_null_not_required = not_null_not_required.isoformat() params["not_null_not_required"] = json_not_null_not_required @@ -53,16 +54,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -73,22 +75,22 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, not_null_required: datetime.datetime, - null_required: Union[None, datetime.datetime], - null_not_required: Union[None, Unset, datetime.datetime] = UNSET, - not_null_not_required: Union[Unset, datetime.datetime] = UNSET, + null_required: datetime.datetime | None, + null_not_required: datetime.datetime | None | Unset = UNSET, + not_null_not_required: datetime.datetime | Unset = UNSET, ) -> Response[Any]: """ Args: not_null_required (datetime.datetime): - null_required (Union[None, datetime.datetime]): - null_not_required (Union[None, Unset, datetime.datetime]): - not_null_not_required (Union[Unset, datetime.datetime]): + null_required (datetime.datetime | None): + null_not_required (datetime.datetime | None | Unset): + not_null_not_required (datetime.datetime | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -110,22 +112,22 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, not_null_required: datetime.datetime, - null_required: Union[None, datetime.datetime], - null_not_required: Union[None, Unset, datetime.datetime] = UNSET, - not_null_not_required: Union[Unset, datetime.datetime] = UNSET, + null_required: datetime.datetime | None, + null_not_required: datetime.datetime | None | Unset = UNSET, + not_null_not_required: datetime.datetime | Unset = UNSET, ) -> Response[Any]: """ Args: not_null_required (datetime.datetime): - null_required (Union[None, datetime.datetime]): - null_not_required (Union[None, Unset, datetime.datetime]): - not_null_not_required (Union[Unset, datetime.datetime]): + null_required (datetime.datetime | None): + null_not_required (datetime.datetime | None | Unset): + not_null_not_required (datetime.datetime | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/hyphen_in_path.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/hyphen_in_path.py similarity index 72% rename from end_to_end_tests/golden-record/my_test_api_client/api/naming/hyphen_in_path.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/hyphen_in_path.py index a0caba2d6..03a1451ff 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/naming/hyphen_in_path.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/hyphen_in_path.py @@ -1,7 +1,8 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any +from urllib.parse import quote -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -11,24 +12,28 @@ def _get_kwargs( hyphen_in_path: str, ) -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", - "url": f"/naming/{hyphen_in_path}", + "url": "/naming/{hyphen_in_path}".format( + hyphen_in_path=quote(str(hyphen_in_path), safe=""), + ), } return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -40,7 +45,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( hyphen_in_path: str, *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """ Args: @@ -48,7 +53,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -68,7 +73,7 @@ def sync_detailed( async def asyncio_detailed( hyphen_in_path: str, *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """ Args: @@ -76,7 +81,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/mixed_case.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/mixed_case.py similarity index 79% rename from end_to_end_tests/golden-record/my_test_api_client/api/naming/mixed_case.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/mixed_case.py index 7df2d318f..a45840f45 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/naming/mixed_case.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/mixed_case.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -14,6 +14,7 @@ def _get_kwargs( mixed_case: str, mixedCase: str, ) -> dict[str, Any]: + params: dict[str, Any] = {} params["mixed_case"] = mixed_case @@ -31,13 +32,12 @@ def _get_kwargs( return _kwargs -def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[MixedCaseResponse200]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> MixedCaseResponse200 | None: if response.status_code == 200: response_200 = MixedCaseResponse200.from_dict(response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -45,7 +45,7 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response + *, client: AuthenticatedClient | Client, response: httpx2.Response ) -> Response[MixedCaseResponse200]: return Response( status_code=HTTPStatus(response.status_code), @@ -57,7 +57,7 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, mixed_case: str, mixedCase: str, ) -> Response[MixedCaseResponse200]: @@ -68,7 +68,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[MixedCaseResponse200] @@ -88,10 +88,10 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, mixed_case: str, mixedCase: str, -) -> Optional[MixedCaseResponse200]: +) -> MixedCaseResponse200 | None: """ Args: mixed_case (str): @@ -99,7 +99,7 @@ def sync( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: MixedCaseResponse200 @@ -114,7 +114,7 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, mixed_case: str, mixedCase: str, ) -> Response[MixedCaseResponse200]: @@ -125,7 +125,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[MixedCaseResponse200] @@ -143,10 +143,10 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, mixed_case: str, mixedCase: str, -) -> Optional[MixedCaseResponse200]: +) -> MixedCaseResponse200 | None: """ Args: mixed_case (str): @@ -154,7 +154,7 @@ async def asyncio( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: MixedCaseResponse200 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py similarity index 66% rename from end_to_end_tests/golden-record/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py index 3bb8b698b..097346097 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/naming/post_naming_property_conflict_with_import.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -9,12 +9,12 @@ from ...models.post_naming_property_conflict_with_import_response_200 import ( PostNamingPropertyConflictWithImportResponse200, ) -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, - body: PostNamingPropertyConflictWithImportBody, + body: PostNamingPropertyConflictWithImportBody | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -23,9 +23,9 @@ def _get_kwargs( "url": "/naming/property-conflict-with-import", } - _body = body.to_dict() + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers @@ -33,12 +33,13 @@ def _get_kwargs( def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[PostNamingPropertyConflictWithImportResponse200]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> PostNamingPropertyConflictWithImportResponse200 | None: if response.status_code == 200: response_200 = PostNamingPropertyConflictWithImportResponse200.from_dict(response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -46,7 +47,7 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response + *, client: AuthenticatedClient | Client, response: httpx2.Response ) -> Response[PostNamingPropertyConflictWithImportResponse200]: return Response( status_code=HTTPStatus(response.status_code), @@ -58,16 +59,16 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], - body: PostNamingPropertyConflictWithImportBody, + client: AuthenticatedClient | Client, + body: PostNamingPropertyConflictWithImportBody | Unset = UNSET, ) -> Response[PostNamingPropertyConflictWithImportResponse200]: """ Args: - body (PostNamingPropertyConflictWithImportBody): + body (PostNamingPropertyConflictWithImportBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[PostNamingPropertyConflictWithImportResponse200] @@ -86,16 +87,16 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], - body: PostNamingPropertyConflictWithImportBody, -) -> Optional[PostNamingPropertyConflictWithImportResponse200]: + client: AuthenticatedClient | Client, + body: PostNamingPropertyConflictWithImportBody | Unset = UNSET, +) -> PostNamingPropertyConflictWithImportResponse200 | None: """ Args: - body (PostNamingPropertyConflictWithImportBody): + body (PostNamingPropertyConflictWithImportBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: PostNamingPropertyConflictWithImportResponse200 @@ -109,16 +110,16 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], - body: PostNamingPropertyConflictWithImportBody, + client: AuthenticatedClient | Client, + body: PostNamingPropertyConflictWithImportBody | Unset = UNSET, ) -> Response[PostNamingPropertyConflictWithImportResponse200]: """ Args: - body (PostNamingPropertyConflictWithImportBody): + body (PostNamingPropertyConflictWithImportBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[PostNamingPropertyConflictWithImportResponse200] @@ -135,16 +136,16 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], - body: PostNamingPropertyConflictWithImportBody, -) -> Optional[PostNamingPropertyConflictWithImportResponse200]: + client: AuthenticatedClient | Client, + body: PostNamingPropertyConflictWithImportBody | Unset = UNSET, +) -> PostNamingPropertyConflictWithImportResponse200 | None: """ Args: - body (PostNamingPropertyConflictWithImportBody): + body (PostNamingPropertyConflictWithImportBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: PostNamingPropertyConflictWithImportResponse200 diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameter_references/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameter_references/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameter_references/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py similarity index 63% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py index e7a8e2712..a5fbec07c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py @@ -1,7 +1,8 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any +from urllib.parse import quote -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -11,10 +12,10 @@ def _get_kwargs( path_param: str, *, - string_param: Union[Unset, str] = UNSET, - integer_param: Union[Unset, int] = 0, - header_param: Union[None, Unset, str] = UNSET, - cookie_param: Union[Unset, str] = UNSET, + string_param: str | Unset = UNSET, + integer_param: int | Unset = 0, + header_param: None | str | Unset = UNSET, + cookie_param: str | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} if not isinstance(header_param, Unset): @@ -34,7 +35,9 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": f"/parameter-references/{path_param}", + "url": "/parameter-references/{path_param}".format( + path_param=quote(str(path_param), safe=""), + ), "params": params, "cookies": cookies, } @@ -43,16 +46,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -64,24 +68,24 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( path_param: str, *, - client: Union[AuthenticatedClient, Client], - string_param: Union[Unset, str] = UNSET, - integer_param: Union[Unset, int] = 0, - header_param: Union[None, Unset, str] = UNSET, - cookie_param: Union[Unset, str] = UNSET, + client: AuthenticatedClient | Client, + string_param: str | Unset = UNSET, + integer_param: int | Unset = 0, + header_param: None | str | Unset = UNSET, + cookie_param: str | Unset = UNSET, ) -> Response[Any]: """Test different types of parameter references Args: path_param (str): - string_param (Union[Unset, str]): - integer_param (Union[Unset, int]): Default: 0. - header_param (Union[None, Unset, str]): - cookie_param (Union[Unset, str]): + string_param (str | Unset): + integer_param (int | Unset): Default: 0. + header_param (None | str | Unset): + cookie_param (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -105,24 +109,24 @@ def sync_detailed( async def asyncio_detailed( path_param: str, *, - client: Union[AuthenticatedClient, Client], - string_param: Union[Unset, str] = UNSET, - integer_param: Union[Unset, int] = 0, - header_param: Union[None, Unset, str] = UNSET, - cookie_param: Union[Unset, str] = UNSET, + client: AuthenticatedClient | Client, + string_param: str | Unset = UNSET, + integer_param: int | Unset = 0, + header_param: None | str | Unset = UNSET, + cookie_param: str | Unset = UNSET, ) -> Response[Any]: """Test different types of parameter references Args: path_param (str): - string_param (Union[Unset, str]): - integer_param (Union[Unset, int]): Default: 0. - header_param (Union[None, Unset, str]): - cookie_param (Union[Unset, str]): + string_param (str | Unset): + integer_param (int | Unset): Default: 0. + header_param (None | str | Unset): + cookie_param (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py similarity index 70% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py index 704996107..f6d28f607 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py @@ -1,7 +1,8 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any +from urllib.parse import quote -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -11,8 +12,9 @@ def _get_kwargs( param_path: str, *, - param_query: Union[Unset, str] = UNSET, + param_query: str | Unset = UNSET, ) -> dict[str, Any]: + params: dict[str, Any] = {} params["param"] = param_query @@ -21,23 +23,26 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "delete", - "url": f"/common_parameters_overriding/{param_path}", + "url": "/common_parameters_overriding/{param_path}".format( + param_path=quote(str(param_path), safe=""), + ), "params": params, } return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -49,17 +54,17 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( param_path: str, *, - client: Union[AuthenticatedClient, Client], - param_query: Union[Unset, str] = UNSET, + client: AuthenticatedClient | Client, + param_query: str | Unset = UNSET, ) -> Response[Any]: """ Args: param_path (str): - param_query (Union[Unset, str]): + param_query (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -80,17 +85,17 @@ def sync_detailed( async def asyncio_detailed( param_path: str, *, - client: Union[AuthenticatedClient, Client], - param_query: Union[Unset, str] = UNSET, + client: AuthenticatedClient | Client, + param_query: str | Unset = UNSET, ) -> Response[Any]: """ Args: param_path (str): - param_query (Union[Unset, str]): + param_query (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py similarity index 79% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py index b6efbba9b..87d43132a 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py @@ -1,7 +1,8 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any +from urllib.parse import quote -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -13,6 +14,7 @@ def _get_kwargs( *, param_query: str = "overridden_in_GET", ) -> dict[str, Any]: + params: dict[str, Any] = {} params["param"] = param_query @@ -21,23 +23,26 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": f"/common_parameters_overriding/{param_path}", + "url": "/common_parameters_overriding/{param_path}".format( + param_path=quote(str(param_path), safe=""), + ), "params": params, } return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -49,7 +54,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( param_path: str, *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, param_query: str = "overridden_in_GET", ) -> Response[Any]: """Test that if you have an overriding property from `PathItem` in `Operation`, it produces valid code @@ -61,7 +66,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -82,7 +87,7 @@ def sync_detailed( async def asyncio_detailed( param_path: str, *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, param_query: str = "overridden_in_GET", ) -> Response[Any]: """Test that if you have an overriding property from `PathItem` in `Operation`, it produces valid code @@ -94,7 +99,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py similarity index 65% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py index 6a7ed7fd5..4399d04a5 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py @@ -1,7 +1,8 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any +from urllib.parse import quote -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -11,9 +12,9 @@ def _get_kwargs( param_path: str, *, - param_query: Union[Unset, str] = UNSET, - param_header: Union[Unset, str] = UNSET, - param_cookie: Union[Unset, str] = UNSET, + param_query: str | Unset = UNSET, + param_header: str | Unset = UNSET, + param_cookie: str | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} if not isinstance(param_header, Unset): @@ -31,7 +32,9 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": f"/same-name-multiple-locations/{param_path}", + "url": "/same-name-multiple-locations/{param_path}".format( + param_path=quote(str(param_path), safe=""), + ), "params": params, "cookies": cookies, } @@ -40,16 +43,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -61,21 +65,21 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( param_path: str, *, - client: Union[AuthenticatedClient, Client], - param_query: Union[Unset, str] = UNSET, - param_header: Union[Unset, str] = UNSET, - param_cookie: Union[Unset, str] = UNSET, + client: AuthenticatedClient | Client, + param_query: str | Unset = UNSET, + param_header: str | Unset = UNSET, + param_cookie: str | Unset = UNSET, ) -> Response[Any]: """ Args: param_path (str): - param_query (Union[Unset, str]): - param_header (Union[Unset, str]): - param_cookie (Union[Unset, str]): + param_query (str | Unset): + param_header (str | Unset): + param_cookie (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -98,21 +102,21 @@ def sync_detailed( async def asyncio_detailed( param_path: str, *, - client: Union[AuthenticatedClient, Client], - param_query: Union[Unset, str] = UNSET, - param_header: Union[Unset, str] = UNSET, - param_cookie: Union[Unset, str] = UNSET, + client: AuthenticatedClient | Client, + param_query: str | Unset = UNSET, + param_header: str | Unset = UNSET, + param_cookie: str | Unset = UNSET, ) -> Response[Any]: """ Args: param_path (str): - param_query (Union[Unset, str]): - param_header (Union[Unset, str]): - param_cookie (Union[Unset, str]): + param_query (str | Unset): + param_header (str | Unset): + param_cookie (str | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/multiple_path_parameters.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/multiple_path_parameters.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/api/parameters/multiple_path_parameters.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/multiple_path_parameters.py index 44345aa26..06d727608 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/parameters/multiple_path_parameters.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/parameters/multiple_path_parameters.py @@ -1,7 +1,8 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any +from urllib.parse import quote -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -14,24 +15,31 @@ def _get_kwargs( param1: str, param3: int, ) -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", - "url": f"/multiple-path-parameters/{param4}/something/{param2}/{param1}/{param3}", + "url": "/multiple-path-parameters/{param4}/something/{param2}/{param1}/{param3}".format( + param4=quote(str(param4), safe=""), + param2=quote(str(param2), safe=""), + param1=quote(str(param1), safe=""), + param3=quote(str(param3), safe=""), + ), } return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -46,7 +54,7 @@ def sync_detailed( param1: str, param3: int, *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """ Args: @@ -57,7 +65,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -83,7 +91,7 @@ async def asyncio_detailed( param1: str, param3: int, *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """ Args: @@ -94,7 +102,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/default_status_code.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/default_status_code.py new file mode 100644 index 000000000..220bdc417 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/default_status_code.py @@ -0,0 +1,115 @@ +from http import HTTPStatus +from typing import Any + +import httpx2 + +from ...client import AuthenticatedClient, Client +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/responses/status-codes/default", + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> str: + response_default = response.text + return response_default + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[str]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[str]: + """Default Status Code Only + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[str] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, +) -> str | None: + """Default Status Code Only + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + str + """ + + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[str]: + """Default Status Code Only + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[str] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, +) -> str | None: + """Default Status Code Only + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + str + """ + + return ( + await asyncio_detailed( + client=client, + ) + ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/responses/post_responses_unions_simple_before_complex.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/post_responses_unions_simple_before_complex.py similarity index 75% rename from end_to_end_tests/golden-record/my_test_api_client/api/responses/post_responses_unions_simple_before_complex.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/post_responses_unions_simple_before_complex.py index cf0599306..ba8a64942 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/responses/post_responses_unions_simple_before_complex.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/post_responses_unions_simple_before_complex.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -12,6 +12,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "post", "url": "/responses/unions/simple_before_complex", @@ -21,12 +22,13 @@ def _get_kwargs() -> dict[str, Any]: def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[PostResponsesUnionsSimpleBeforeComplexResponse200]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> PostResponsesUnionsSimpleBeforeComplexResponse200 | None: if response.status_code == 200: response_200 = PostResponsesUnionsSimpleBeforeComplexResponse200.from_dict(response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -34,7 +36,7 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response + *, client: AuthenticatedClient | Client, response: httpx2.Response ) -> Response[PostResponsesUnionsSimpleBeforeComplexResponse200]: return Response( status_code=HTTPStatus(response.status_code), @@ -46,13 +48,13 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[PostResponsesUnionsSimpleBeforeComplexResponse200]: """Regression test for #603 Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[PostResponsesUnionsSimpleBeforeComplexResponse200] @@ -69,13 +71,13 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[PostResponsesUnionsSimpleBeforeComplexResponse200]: + client: AuthenticatedClient | Client, +) -> PostResponsesUnionsSimpleBeforeComplexResponse200 | None: """Regression test for #603 Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: PostResponsesUnionsSimpleBeforeComplexResponse200 @@ -88,13 +90,13 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[PostResponsesUnionsSimpleBeforeComplexResponse200]: """Regression test for #603 Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[PostResponsesUnionsSimpleBeforeComplexResponse200] @@ -109,13 +111,13 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[PostResponsesUnionsSimpleBeforeComplexResponse200]: + client: AuthenticatedClient | Client, +) -> PostResponsesUnionsSimpleBeforeComplexResponse200 | None: """Regression test for #603 Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: PostResponsesUnionsSimpleBeforeComplexResponse200 diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/reference_response.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/reference_response.py new file mode 100644 index 000000000..1d5c6e119 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/reference_response.py @@ -0,0 +1,124 @@ +from http import HTTPStatus +from typing import Any + +import httpx2 + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.a_model import AModel +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/responses/reference", + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> AModel | None: + if response.status_code == 200: + response_200 = AModel.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[AModel]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[AModel]: + """Endpoint using predefined response + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[AModel] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, +) -> AModel | None: + """Endpoint using predefined response + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + AModel + """ + + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[AModel]: + """Endpoint using predefined response + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[AModel] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, +) -> AModel | None: + """Endpoint using predefined response + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + AModel + """ + + return ( + await asyncio_detailed( + client=client, + ) + ).parsed diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/status_code_patterns.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/status_code_patterns.py new file mode 100644 index 000000000..094edac13 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/status_code_patterns.py @@ -0,0 +1,134 @@ +from http import HTTPStatus +from typing import Any + +import httpx2 + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.status_code_patterns_response_2xx import StatusCodePatternsResponse2XX +from ...models.status_code_patterns_response_4xx import StatusCodePatternsResponse4XX +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/response/status-codes/patterns", + } + + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> StatusCodePatternsResponse2XX | StatusCodePatternsResponse4XX | None: + if 200 <= response.status_code <= 299: + response_2xx = StatusCodePatternsResponse2XX.from_dict(response.json()) + + return response_2xx + + if 400 <= response.status_code <= 499: + response_4xx = StatusCodePatternsResponse4XX.from_dict(response.json()) + + return response_4xx + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Response[StatusCodePatternsResponse2XX | StatusCodePatternsResponse4XX]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[StatusCodePatternsResponse2XX | StatusCodePatternsResponse4XX]: + """Status Code Patterns + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[StatusCodePatternsResponse2XX | StatusCodePatternsResponse4XX] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, +) -> StatusCodePatternsResponse2XX | StatusCodePatternsResponse4XX | None: + """Status Code Patterns + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + StatusCodePatternsResponse2XX | StatusCodePatternsResponse4XX + """ + + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[StatusCodePatternsResponse2XX | StatusCodePatternsResponse4XX]: + """Status Code Patterns + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[StatusCodePatternsResponse2XX | StatusCodePatternsResponse4XX] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, +) -> StatusCodePatternsResponse2XX | StatusCodePatternsResponse4XX | None: + """Status Code Patterns + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + StatusCodePatternsResponse2XX | StatusCodePatternsResponse4XX + """ + + return ( + await asyncio_detailed( + client=client, + ) + ).parsed diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/status_code_precedence.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/status_code_precedence.py new file mode 100644 index 000000000..c836a8bf6 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/status_code_precedence.py @@ -0,0 +1,135 @@ +from http import HTTPStatus +from typing import Any + +import httpx2 + +from ...client import AuthenticatedClient, Client +from ...types import Response + + +def _get_kwargs() -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/response/status-codes/precedence", + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> str: + if response.status_code == 200: + response_200 = response.text + return response_200 + + if response.status_code == 404: + response_404 = response.text + return response_404 + + if 400 <= response.status_code <= 499: + response_4xx = response.text + return response_4xx + + response_default = response.text + return response_default + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[str]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[str]: + """Status Codes Precedence + + Verify that specific status codes are always checked first, then ranges, then default + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[str] + """ + + kwargs = _get_kwargs() + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, +) -> str | None: + """Status Codes Precedence + + Verify that specific status codes are always checked first, then ranges, then default + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + str + """ + + return sync_detailed( + client=client, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, +) -> Response[str]: + """Status Codes Precedence + + Verify that specific status codes are always checked first, then ranges, then default + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[str] + """ + + kwargs = _get_kwargs() + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, +) -> str | None: + """Status Codes Precedence + + Verify that specific status codes are always checked first, then ranges, then default + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx2.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + str + """ + + return ( + await asyncio_detailed( + client=client, + ) + ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/responses/text_response.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/text_response.py similarity index 72% rename from end_to_end_tests/golden-record/my_test_api_client/api/responses/text_response.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/text_response.py index 057ceb2de..f91776e62 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/responses/text_response.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/responses/text_response.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -9,6 +9,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "post", "url": "/responses/text", @@ -17,17 +18,18 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[str]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> str | None: if response.status_code == 200: response_200 = response.text return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[str]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[str]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -38,13 +40,13 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[str]: """Text Response Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[str] @@ -61,13 +63,13 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[str]: + client: AuthenticatedClient | Client, +) -> str | None: """Text Response Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: str @@ -80,13 +82,13 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[str]: """Text Response Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[str] @@ -101,13 +103,13 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[str]: + client: AuthenticatedClient | Client, +) -> str | None: """Text Response Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: str diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag1/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag1/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag1/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tag2/get_tag_with_number.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag1/get_tag_with_number.py similarity index 74% rename from end_to_end_tests/golden-record/my_test_api_client/api/tag2/get_tag_with_number.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag1/get_tag_with_number.py index 62631355f..84438ceba 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tag2/get_tag_with_number.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag1/get_tag_with_number.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -9,6 +9,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/tag_with_number", @@ -17,16 +18,17 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -37,12 +39,12 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -59,12 +61,12 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag2/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag2/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag2/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tag1/get_tag_with_number.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag2/get_tag_with_number.py similarity index 74% rename from end_to_end_tests/golden-record/my_test_api_client/api/tag1/get_tag_with_number.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag2/get_tag_with_number.py index 62631355f..84438ceba 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tag1/get_tag_with_number.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tag2/get_tag_with_number.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -9,6 +9,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/tag_with_number", @@ -17,16 +18,17 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -37,12 +39,12 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -59,12 +61,12 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/callback_test.py similarity index 72% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/callback_test.py index 0852815d2..2f3a83935 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/callback_test.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/callback_test.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -21,9 +21,8 @@ def _get_kwargs( "url": "/tests/callback", } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers @@ -31,15 +30,17 @@ def _get_kwargs( def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Union[Any, HTTPValidationError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Any | HTTPValidationError | None: if response.status_code == 200: response_200 = response.json() return response_200 + if response.status_code == 422: response_422 = HTTPValidationError.from_dict(response.json()) return response_422 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -47,8 +48,8 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Union[Any, HTTPValidationError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Response[Any | HTTPValidationError]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -59,9 +60,9 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: AModel, -) -> Response[Union[Any, HTTPValidationError]]: +) -> Response[Any | HTTPValidationError]: """Path with callback Try sending a request related to a callback @@ -71,10 +72,10 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[Any, HTTPValidationError]] + Response[Any | HTTPValidationError] """ kwargs = _get_kwargs( @@ -90,9 +91,9 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: AModel, -) -> Optional[Union[Any, HTTPValidationError]]: +) -> Any | HTTPValidationError | None: """Path with callback Try sending a request related to a callback @@ -102,10 +103,10 @@ def sync( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[Any, HTTPValidationError] + Any | HTTPValidationError """ return sync_detailed( @@ -116,9 +117,9 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: AModel, -) -> Response[Union[Any, HTTPValidationError]]: +) -> Response[Any | HTTPValidationError]: """Path with callback Try sending a request related to a callback @@ -128,10 +129,10 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[Any, HTTPValidationError]] + Response[Any | HTTPValidationError] """ kwargs = _get_kwargs( @@ -145,9 +146,9 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: AModel, -) -> Optional[Union[Any, HTTPValidationError]]: +) -> Any | HTTPValidationError | None: """Path with callback Try sending a request related to a callback @@ -157,10 +158,10 @@ async def asyncio( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[Any, HTTPValidationError] + Any | HTTPValidationError """ return ( diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/description_with_backslash.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/description_with_backslash.py similarity index 70% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/description_with_backslash.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/description_with_backslash.py index e7cd44f70..225311273 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/description_with_backslash.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/description_with_backslash.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -9,6 +9,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/tests/description-with-backslash", @@ -17,16 +18,17 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -37,15 +39,15 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: - r""" Test description with \ + """ Test description with \\ - Test description with \ + Test description with \\ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -62,15 +64,15 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: - r""" Test description with \ + """ Test description with \\ - Test description with \ + Test description with \\ Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_booleans.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_booleans.py similarity index 73% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_booleans.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_booleans.py index 147eed3a7..cb0000213 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_booleans.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_booleans.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union, cast +from typing import Any, cast -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -9,6 +9,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/tests/basic_lists/booleans", @@ -17,18 +18,19 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[list[bool]]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> list[bool] | None: if response.status_code == 200: response_200 = cast(list[bool], response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[list[bool]]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[list[bool]]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -39,7 +41,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[list[bool]]: """Get Basic List Of Booleans @@ -47,7 +49,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[list[bool]] @@ -64,15 +66,15 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[list[bool]]: + client: AuthenticatedClient | Client, +) -> list[bool] | None: """Get Basic List Of Booleans Get a list of booleans Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: list[bool] @@ -85,7 +87,7 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[list[bool]]: """Get Basic List Of Booleans @@ -93,7 +95,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[list[bool]] @@ -108,15 +110,15 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[list[bool]]: + client: AuthenticatedClient | Client, +) -> list[bool] | None: """Get Basic List Of Booleans Get a list of booleans Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: list[bool] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_floats.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_floats.py similarity index 73% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_floats.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_floats.py index 02b3abb1f..08ec7e33c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_floats.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_floats.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union, cast +from typing import Any, cast -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -9,6 +9,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/tests/basic_lists/floats", @@ -17,18 +18,19 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[list[float]]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> list[float] | None: if response.status_code == 200: response_200 = cast(list[float], response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[list[float]]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[list[float]]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -39,7 +41,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[list[float]]: """Get Basic List Of Floats @@ -47,7 +49,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[list[float]] @@ -64,15 +66,15 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[list[float]]: + client: AuthenticatedClient | Client, +) -> list[float] | None: """Get Basic List Of Floats Get a list of floats Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: list[float] @@ -85,7 +87,7 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[list[float]]: """Get Basic List Of Floats @@ -93,7 +95,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[list[float]] @@ -108,15 +110,15 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[list[float]]: + client: AuthenticatedClient | Client, +) -> list[float] | None: """Get Basic List Of Floats Get a list of floats Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: list[float] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_integers.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_integers.py similarity index 73% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_integers.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_integers.py index e71537363..b3553d133 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_integers.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_integers.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union, cast +from typing import Any, cast -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -9,6 +9,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/tests/basic_lists/integers", @@ -17,18 +18,19 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[list[int]]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> list[int] | None: if response.status_code == 200: response_200 = cast(list[int], response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[list[int]]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[list[int]]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -39,7 +41,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[list[int]]: """Get Basic List Of Integers @@ -47,7 +49,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[list[int]] @@ -64,15 +66,15 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[list[int]]: + client: AuthenticatedClient | Client, +) -> list[int] | None: """Get Basic List Of Integers Get a list of integers Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: list[int] @@ -85,7 +87,7 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[list[int]]: """Get Basic List Of Integers @@ -93,7 +95,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[list[int]] @@ -108,15 +110,15 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[list[int]]: + client: AuthenticatedClient | Client, +) -> list[int] | None: """Get Basic List Of Integers Get a list of integers Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: list[int] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_strings.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_strings.py similarity index 73% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_strings.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_strings.py index 70f153829..a87b9f8ee 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_strings.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_basic_list_of_strings.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union, cast +from typing import Any, cast -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -9,6 +9,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/tests/basic_lists/strings", @@ -17,18 +18,19 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[list[str]]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> list[str] | None: if response.status_code == 200: response_200 = cast(list[str], response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[list[str]]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[list[str]]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -39,7 +41,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[list[str]]: """Get Basic List Of Strings @@ -47,7 +49,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[list[str]] @@ -64,15 +66,15 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[list[str]]: + client: AuthenticatedClient | Client, +) -> list[str] | None: """Get Basic List Of Strings Get a list of strings Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: list[str] @@ -85,7 +87,7 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[list[str]]: """Get Basic List Of Strings @@ -93,7 +95,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[list[str]] @@ -108,15 +110,15 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[list[str]]: + client: AuthenticatedClient | Client, +) -> list[str] | None: """Get Basic List Of Strings Get a list of strings Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: list[str] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_user_list.py similarity index 69% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_user_list.py index a708cf71d..22afe9fc6 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/get_user_list.py @@ -1,8 +1,8 @@ import datetime from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -16,10 +16,11 @@ def _get_kwargs( *, an_enum_value: list[AnEnum], - an_enum_value_with_null: list[Union[AnEnumWithNull, None]], + an_enum_value_with_null: list[AnEnumWithNull | None], an_enum_value_with_only_null: list[None], - some_date: Union[datetime.date, datetime.datetime], + some_date: datetime.date | datetime.datetime, ) -> dict[str, Any]: + params: dict[str, Any] = {} json_an_enum_value = [] @@ -31,7 +32,7 @@ def _get_kwargs( json_an_enum_value_with_null = [] for an_enum_value_with_null_item_data in an_enum_value_with_null: - an_enum_value_with_null_item: Union[None, str] + an_enum_value_with_null_item: None | str if isinstance(an_enum_value_with_null_item_data, AnEnumWithNull): an_enum_value_with_null_item = an_enum_value_with_null_item_data.value else: @@ -64,8 +65,8 @@ def _get_kwargs( def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Union[HTTPValidationError, list["AModel"]]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> HTTPValidationError | list[AModel] | None: if response.status_code == 200: response_200 = [] _response_200 = response.json() @@ -75,14 +76,17 @@ def _parse_response( response_200.append(response_200_item) return response_200 + if response.status_code == 422: response_422 = HTTPValidationError.from_dict(response.json()) return response_422 + if response.status_code == 423: response_423 = HTTPValidationError.from_dict(response.json()) return response_423 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -90,8 +94,8 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Union[HTTPValidationError, list["AModel"]]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Response[HTTPValidationError | list[AModel]]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -102,28 +106,28 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, an_enum_value: list[AnEnum], - an_enum_value_with_null: list[Union[AnEnumWithNull, None]], + an_enum_value_with_null: list[AnEnumWithNull | None], an_enum_value_with_only_null: list[None], - some_date: Union[datetime.date, datetime.datetime], -) -> Response[Union[HTTPValidationError, list["AModel"]]]: + some_date: datetime.date | datetime.datetime, +) -> Response[HTTPValidationError | list[AModel]]: """Get List Get a list of things Args: an_enum_value (list[AnEnum]): - an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): + an_enum_value_with_null (list[AnEnumWithNull | None]): an_enum_value_with_only_null (list[None]): - some_date (Union[datetime.date, datetime.datetime]): + some_date (datetime.date | datetime.datetime): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[HTTPValidationError, list['AModel']]] + Response[HTTPValidationError | list[AModel]] """ kwargs = _get_kwargs( @@ -142,28 +146,28 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, an_enum_value: list[AnEnum], - an_enum_value_with_null: list[Union[AnEnumWithNull, None]], + an_enum_value_with_null: list[AnEnumWithNull | None], an_enum_value_with_only_null: list[None], - some_date: Union[datetime.date, datetime.datetime], -) -> Optional[Union[HTTPValidationError, list["AModel"]]]: + some_date: datetime.date | datetime.datetime, +) -> HTTPValidationError | list[AModel] | None: """Get List Get a list of things Args: an_enum_value (list[AnEnum]): - an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): + an_enum_value_with_null (list[AnEnumWithNull | None]): an_enum_value_with_only_null (list[None]): - some_date (Union[datetime.date, datetime.datetime]): + some_date (datetime.date | datetime.datetime): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[HTTPValidationError, list['AModel']] + HTTPValidationError | list[AModel] """ return sync_detailed( @@ -177,28 +181,28 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, an_enum_value: list[AnEnum], - an_enum_value_with_null: list[Union[AnEnumWithNull, None]], + an_enum_value_with_null: list[AnEnumWithNull | None], an_enum_value_with_only_null: list[None], - some_date: Union[datetime.date, datetime.datetime], -) -> Response[Union[HTTPValidationError, list["AModel"]]]: + some_date: datetime.date | datetime.datetime, +) -> Response[HTTPValidationError | list[AModel]]: """Get List Get a list of things Args: an_enum_value (list[AnEnum]): - an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): + an_enum_value_with_null (list[AnEnumWithNull | None]): an_enum_value_with_only_null (list[None]): - some_date (Union[datetime.date, datetime.datetime]): + some_date (datetime.date | datetime.datetime): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[HTTPValidationError, list['AModel']]] + Response[HTTPValidationError | list[AModel]] """ kwargs = _get_kwargs( @@ -215,28 +219,28 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, an_enum_value: list[AnEnum], - an_enum_value_with_null: list[Union[AnEnumWithNull, None]], + an_enum_value_with_null: list[AnEnumWithNull | None], an_enum_value_with_only_null: list[None], - some_date: Union[datetime.date, datetime.datetime], -) -> Optional[Union[HTTPValidationError, list["AModel"]]]: + some_date: datetime.date | datetime.datetime, +) -> HTTPValidationError | list[AModel] | None: """Get List Get a list of things Args: an_enum_value (list[AnEnum]): - an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): + an_enum_value_with_null (list[AnEnumWithNull | None]): an_enum_value_with_only_null (list[None]): - some_date (Union[datetime.date, datetime.datetime]): + some_date (datetime.date | datetime.datetime): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[HTTPValidationError, list['AModel']] + HTTPValidationError | list[AModel] """ return ( diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/json_body_tests_json_body_post.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/json_body_tests_json_body_post.py index f33a23dc7..75066a876 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/json_body_tests_json_body_post.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -21,9 +21,8 @@ def _get_kwargs( "url": "/tests/json_body", } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers @@ -31,15 +30,17 @@ def _get_kwargs( def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Union[Any, HTTPValidationError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Any | HTTPValidationError | None: if response.status_code == 200: response_200 = response.json() return response_200 + if response.status_code == 422: response_422 = HTTPValidationError.from_dict(response.json()) return response_422 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -47,8 +48,8 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Union[Any, HTTPValidationError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Response[Any | HTTPValidationError]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -59,9 +60,9 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: AModel, -) -> Response[Union[Any, HTTPValidationError]]: +) -> Response[Any | HTTPValidationError]: """Json Body Try sending a JSON body @@ -71,10 +72,10 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[Any, HTTPValidationError]] + Response[Any | HTTPValidationError] """ kwargs = _get_kwargs( @@ -90,9 +91,9 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: AModel, -) -> Optional[Union[Any, HTTPValidationError]]: +) -> Any | HTTPValidationError | None: """Json Body Try sending a JSON body @@ -102,10 +103,10 @@ def sync( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[Any, HTTPValidationError] + Any | HTTPValidationError """ return sync_detailed( @@ -116,9 +117,9 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: AModel, -) -> Response[Union[Any, HTTPValidationError]]: +) -> Response[Any | HTTPValidationError]: """Json Body Try sending a JSON body @@ -128,10 +129,10 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[Any, HTTPValidationError]] + Response[Any | HTTPValidationError] """ kwargs = _get_kwargs( @@ -145,9 +146,9 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: AModel, -) -> Optional[Union[Any, HTTPValidationError]]: +) -> Any | HTTPValidationError | None: """Json Body Try sending a JSON body @@ -157,10 +158,10 @@ async def asyncio( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[Any, HTTPValidationError] + Any | HTTPValidationError """ return ( diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/no_response_tests_no_response_get.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/no_response_tests_no_response_get.py similarity index 74% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/no_response_tests_no_response_get.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/no_response_tests_no_response_get.py index 586947f49..2c7483093 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/no_response_tests_no_response_get.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/no_response_tests_no_response_get.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -9,6 +9,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/tests/no_response", @@ -17,16 +18,17 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -37,13 +39,13 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """No Response Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -60,13 +62,13 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """No Response Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py similarity index 73% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py index efb0f4ae5..cb80638b2 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py @@ -1,8 +1,8 @@ from http import HTTPStatus from io import BytesIO -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -10,6 +10,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/tests/octet_stream", @@ -18,18 +19,19 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[File]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> File | None: if response.status_code == 200: response_200 = File(payload=BytesIO(response.content)) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[File]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[File]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -40,13 +42,13 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[File]: """Octet Stream Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[File] @@ -63,13 +65,13 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[File]: + client: AuthenticatedClient | Client, +) -> File | None: """Octet Stream Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: File @@ -82,13 +84,13 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[File]: """Octet Stream Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[File] @@ -103,13 +105,13 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], -) -> Optional[File]: + client: AuthenticatedClient | Client, +) -> File | None: """Octet Stream Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: File diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py similarity index 55% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py index ea0cbd65a..05de63067 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/octet_stream_tests_octet_stream_post.py @@ -1,17 +1,18 @@ from http import HTTPStatus -from typing import Any, Optional, Union, cast +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client from ...models.http_validation_error import HTTPValidationError -from ...types import File, Response +from ...models.octet_stream_tests_octet_stream_post_response_200 import OctetStreamTestsOctetStreamPostResponse200 +from ...types import UNSET, File, Response, Unset def _get_kwargs( *, - body: File, + body: File | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -20,9 +21,8 @@ def _get_kwargs( "url": "/tests/octet_stream", } - _body = body.payload - - _kwargs["content"] = _body + if not isinstance(body, Unset): + _kwargs["content"] = body.payload headers["Content-Type"] = "application/octet-stream" _kwargs["headers"] = headers @@ -30,15 +30,18 @@ def _get_kwargs( def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Union[HTTPValidationError, str]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> HTTPValidationError | OctetStreamTestsOctetStreamPostResponse200 | None: if response.status_code == 200: - response_200 = cast(str, response.json()) + response_200 = OctetStreamTestsOctetStreamPostResponse200.from_dict(response.json()) + return response_200 + if response.status_code == 422: response_422 = HTTPValidationError.from_dict(response.json()) return response_422 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -46,8 +49,8 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Union[HTTPValidationError, str]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Response[HTTPValidationError | OctetStreamTestsOctetStreamPostResponse200]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -58,20 +61,20 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], - body: File, -) -> Response[Union[HTTPValidationError, str]]: + client: AuthenticatedClient | Client, + body: File | Unset = UNSET, +) -> Response[HTTPValidationError | OctetStreamTestsOctetStreamPostResponse200]: """Binary (octet stream) request body Args: - body (File): A file to upload + body (File | Unset): A file to upload Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[HTTPValidationError, str]] + Response[HTTPValidationError | OctetStreamTestsOctetStreamPostResponse200] """ kwargs = _get_kwargs( @@ -87,20 +90,20 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], - body: File, -) -> Optional[Union[HTTPValidationError, str]]: + client: AuthenticatedClient | Client, + body: File | Unset = UNSET, +) -> HTTPValidationError | OctetStreamTestsOctetStreamPostResponse200 | None: """Binary (octet stream) request body Args: - body (File): A file to upload + body (File | Unset): A file to upload Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[HTTPValidationError, str] + HTTPValidationError | OctetStreamTestsOctetStreamPostResponse200 """ return sync_detailed( @@ -111,20 +114,20 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], - body: File, -) -> Response[Union[HTTPValidationError, str]]: + client: AuthenticatedClient | Client, + body: File | Unset = UNSET, +) -> Response[HTTPValidationError | OctetStreamTestsOctetStreamPostResponse200]: """Binary (octet stream) request body Args: - body (File): A file to upload + body (File | Unset): A file to upload Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[HTTPValidationError, str]] + Response[HTTPValidationError | OctetStreamTestsOctetStreamPostResponse200] """ kwargs = _get_kwargs( @@ -138,20 +141,20 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], - body: File, -) -> Optional[Union[HTTPValidationError, str]]: + client: AuthenticatedClient | Client, + body: File | Unset = UNSET, +) -> HTTPValidationError | OctetStreamTestsOctetStreamPostResponse200 | None: """Binary (octet stream) request body Args: - body (File): A file to upload + body (File | Unset): A file to upload Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[HTTPValidationError, str] + HTTPValidationError | OctetStreamTestsOctetStreamPostResponse200 """ return ( diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_form_data.py similarity index 77% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_form_data.py index ec65d0363..6a16a08ed 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_form_data.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -20,25 +20,24 @@ def _get_kwargs( "url": "/tests/post_form_data", } - _body = body.to_dict() - - _kwargs["data"] = _body + _kwargs["data"] = body.to_dict() headers["Content-Type"] = "application/x-www-form-urlencoded" _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -49,7 +48,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: AFormData, ) -> Response[Any]: """Post form data @@ -61,7 +60,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -80,7 +79,7 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: AFormData, ) -> Response[Any]: """Post form data @@ -92,7 +91,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data_inline.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_form_data_inline.py similarity index 78% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data_inline.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_form_data_inline.py index bc5ad7cc4..06591c97f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_form_data_inline.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_form_data_inline.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -20,25 +20,24 @@ def _get_kwargs( "url": "/tests/post_form_data_inline", } - _body = body.to_dict() - - _kwargs["data"] = _body + _kwargs["data"] = body.to_dict() headers["Content-Type"] = "application/x-www-form-urlencoded" _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -49,7 +48,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: PostFormDataInlineBody, ) -> Response[Any]: """Post form data (inline schema) @@ -61,7 +60,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -80,7 +79,7 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: PostFormDataInlineBody, ) -> Response[Any]: """Post form data (inline schema) @@ -92,7 +91,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_tests_json_body_string.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_tests_json_body_string.py similarity index 69% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/post_tests_json_body_string.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_tests_json_body_string.py index ba40de26f..76a715e00 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/post_tests_json_body_string.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/post_tests_json_body_string.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union, cast +from typing import Any, cast -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -20,9 +20,8 @@ def _get_kwargs( "url": "/tests/json_body/string", } - _body = body + _kwargs["json"] = body - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers @@ -30,15 +29,17 @@ def _get_kwargs( def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Union[HTTPValidationError, str]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> HTTPValidationError | str | None: if response.status_code == 200: response_200 = cast(str, response.json()) return response_200 + if response.status_code == 422: response_422 = HTTPValidationError.from_dict(response.json()) return response_422 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -46,8 +47,8 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Union[HTTPValidationError, str]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Response[HTTPValidationError | str]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -58,9 +59,9 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: str, -) -> Response[Union[HTTPValidationError, str]]: +) -> Response[HTTPValidationError | str]: """Json Body Which is String Args: @@ -68,10 +69,10 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[HTTPValidationError, str]] + Response[HTTPValidationError | str] """ kwargs = _get_kwargs( @@ -87,9 +88,9 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: str, -) -> Optional[Union[HTTPValidationError, str]]: +) -> HTTPValidationError | str | None: """Json Body Which is String Args: @@ -97,10 +98,10 @@ def sync( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[HTTPValidationError, str] + HTTPValidationError | str """ return sync_detailed( @@ -111,9 +112,9 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: str, -) -> Response[Union[HTTPValidationError, str]]: +) -> Response[HTTPValidationError | str]: """Json Body Which is String Args: @@ -121,10 +122,10 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[HTTPValidationError, str]] + Response[HTTPValidationError | str] """ kwargs = _get_kwargs( @@ -138,9 +139,9 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: str, -) -> Optional[Union[HTTPValidationError, str]]: +) -> HTTPValidationError | str | None: """Json Body Which is String Args: @@ -148,10 +149,10 @@ async def asyncio( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[HTTPValidationError, str] + HTTPValidationError | str """ return ( diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/test_inline_objects.py similarity index 78% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/test_inline_objects.py index 287ea4a1a..baf632f64 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/test_inline_objects.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -21,9 +21,8 @@ def _get_kwargs( "url": "/tests/inline_objects", } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers @@ -31,12 +30,13 @@ def _get_kwargs( def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[TestInlineObjectsResponse200]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> TestInlineObjectsResponse200 | None: if response.status_code == 200: response_200 = TestInlineObjectsResponse200.from_dict(response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -44,7 +44,7 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response + *, client: AuthenticatedClient | Client, response: httpx2.Response ) -> Response[TestInlineObjectsResponse200]: return Response( status_code=HTTPStatus(response.status_code), @@ -56,7 +56,7 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: TestInlineObjectsBody, ) -> Response[TestInlineObjectsResponse200]: """Test Inline Objects @@ -66,7 +66,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[TestInlineObjectsResponse200] @@ -85,9 +85,9 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: TestInlineObjectsBody, -) -> Optional[TestInlineObjectsResponse200]: +) -> TestInlineObjectsResponse200 | None: """Test Inline Objects Args: @@ -95,7 +95,7 @@ def sync( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: TestInlineObjectsResponse200 @@ -109,7 +109,7 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: TestInlineObjectsBody, ) -> Response[TestInlineObjectsResponse200]: """Test Inline Objects @@ -119,7 +119,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[TestInlineObjectsResponse200] @@ -136,9 +136,9 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: TestInlineObjectsBody, -) -> Optional[TestInlineObjectsResponse200]: +) -> TestInlineObjectsResponse200 | None: """Test Inline Objects Args: @@ -146,7 +146,7 @@ async def asyncio( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: TestInlineObjectsResponse200 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/token_with_cookie_auth_token_with_cookie_get.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/token_with_cookie_auth_token_with_cookie_get.py similarity index 78% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/token_with_cookie_auth_token_with_cookie_get.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/token_with_cookie_auth_token_with_cookie_get.py index 22ac00650..d97c6863d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/token_with_cookie_auth_token_with_cookie_get.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/token_with_cookie_auth_token_with_cookie_get.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -12,6 +12,7 @@ def _get_kwargs( *, my_token: str, ) -> dict[str, Any]: + cookies = {} cookies["MyToken"] = my_token @@ -24,18 +25,20 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if response.status_code == 401: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -46,7 +49,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, my_token: str, ) -> Response[Any]: """TOKEN_WITH_COOKIE @@ -58,7 +61,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -77,7 +80,7 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, my_token: str, ) -> Response[Any]: """TOKEN_WITH_COOKIE @@ -89,7 +92,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py similarity index 74% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py index 61e8434e6..ada25d673 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -9,6 +9,7 @@ def _get_kwargs() -> dict[str, Any]: + _kwargs: dict[str, Any] = { "method": "get", "url": "/tests/unsupported_content", @@ -17,16 +18,17 @@ def _get_kwargs() -> dict[str, Any]: return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -37,13 +39,13 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """Unsupported Content Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -60,13 +62,13 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, ) -> Response[Any]: """Unsupported Content Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/upload_file_tests_upload_post.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 9f1864ec3..cb12c9968 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -21,24 +21,24 @@ def _get_kwargs( "url": "/tests/upload", } - _body = body.to_multipart() - - _kwargs["files"] = _body + _kwargs["files"] = body.to_multipart() _kwargs["headers"] = headers return _kwargs def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Union[Any, HTTPValidationError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Any | HTTPValidationError | None: if response.status_code == 200: response_200 = response.json() return response_200 + if response.status_code == 422: response_422 = HTTPValidationError.from_dict(response.json()) return response_422 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -46,8 +46,8 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Union[Any, HTTPValidationError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Response[Any | HTTPValidationError]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -58,9 +58,9 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: BodyUploadFileTestsUploadPost, -) -> Response[Union[Any, HTTPValidationError]]: +) -> Response[Any | HTTPValidationError]: """Upload File Upload a file @@ -70,10 +70,10 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[Any, HTTPValidationError]] + Response[Any | HTTPValidationError] """ kwargs = _get_kwargs( @@ -89,9 +89,9 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: BodyUploadFileTestsUploadPost, -) -> Optional[Union[Any, HTTPValidationError]]: +) -> Any | HTTPValidationError | None: """Upload File Upload a file @@ -101,10 +101,10 @@ def sync( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[Any, HTTPValidationError] + Any | HTTPValidationError """ return sync_detailed( @@ -115,9 +115,9 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: BodyUploadFileTestsUploadPost, -) -> Response[Union[Any, HTTPValidationError]]: +) -> Response[Any | HTTPValidationError]: """Upload File Upload a file @@ -127,10 +127,10 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[Any, HTTPValidationError]] + Response[Any | HTTPValidationError] """ kwargs = _get_kwargs( @@ -144,9 +144,9 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: BodyUploadFileTestsUploadPost, -) -> Optional[Union[Any, HTTPValidationError]]: +) -> Any | HTTPValidationError | None: """Upload File Upload a file @@ -156,10 +156,10 @@ async def asyncio( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[Any, HTTPValidationError] + Any | HTTPValidationError """ return ( diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/true_/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/true_/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/true_/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/true_/false_.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/true_/false_.py similarity index 78% rename from end_to_end_tests/golden-record/my_test_api_client/api/true_/false_.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/true_/false_.py index b46550153..8a00ad453 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/true_/false_.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/api/true_/false_.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -12,6 +12,7 @@ def _get_kwargs( *, import_: str, ) -> dict[str, Any]: + params: dict[str, Any] = {} params["import"] = import_ @@ -27,16 +28,17 @@ def _get_kwargs( return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Any | None: if response.status_code == 200: return None + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[Any]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -47,7 +49,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, import_: str, ) -> Response[Any]: """ @@ -56,7 +58,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] @@ -75,7 +77,7 @@ def sync_detailed( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, import_: str, ) -> Response[Any]: """ @@ -84,7 +86,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Any] diff --git a/end_to_end_tests/golden-record/my_test_api_client/client.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/client.py similarity index 72% rename from end_to_end_tests/golden-record/my_test_api_client/client.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/client.py index e80446f10..136b83d06 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/client.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/client.py @@ -1,7 +1,7 @@ import ssl -from typing import Any, Optional, Union +from typing import Any, Self -import httpx +import httpx2 from attrs import define, evolve, field @@ -9,7 +9,7 @@ class Client: """A class for keeping track of data related to the API - The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: ``base_url``: The base URL for the API, all requests are made to a relative path to this URL @@ -18,14 +18,14 @@ class Client: ``headers``: A dictionary of headers to be sent with every request ``timeout``: The maximum amount of a time a request can take. API functions will raise - httpx.TimeoutException if this is exceeded. + httpx2.TimeoutException if this is exceeded. ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, but can be set to False for testing purposes. ``follow_redirects``: Whether or not to follow redirects. Default value is False. - ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. Attributes: @@ -38,12 +38,12 @@ class Client: _base_url: str = field(alias="base_url") _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") - _client: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) def with_headers(self, headers: dict[str, str]) -> "Client": """Get a new client matching this one with additional headers""" @@ -61,26 +61,26 @@ def with_cookies(self, cookies: dict[str, str]) -> "Client": self._async_client.cookies.update(cookies) return evolve(self, cookies={**self._cookies, **cookies}) - def with_timeout(self, timeout: httpx.Timeout) -> "Client": - """Get a new client matching this one with a new timeout (in seconds)""" + def with_timeout(self, timeout: httpx2.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" if self._client is not None: self._client.timeout = timeout if self._async_client is not None: self._async_client.timeout = timeout return evolve(self, timeout=timeout) - def set_httpx_client(self, client: httpx.Client) -> "Client": - """Manually set the underlying httpx.Client + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._client = client return self - def get_httpx_client(self) -> httpx.Client: - """Get the underlying httpx.Client, constructing a new one if not previously set""" + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" if self._client is None: - self._client = httpx.Client( + self._client = httpx2.Client( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -91,27 +91,27 @@ def get_httpx_client(self) -> httpx.Client: ) return self._client - def __enter__(self) -> "Client": - """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" self.get_httpx_client().__enter__() return self def __exit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for internal httpx.Client (see httpx docs)""" + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" self.get_httpx_client().__exit__(*args, **kwargs) - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client": - """Manually the underlying httpx.AsyncClient + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._async_client = async_client return self - def get_async_httpx_client(self) -> httpx.AsyncClient: - """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" if self._async_client is None: - self._async_client = httpx.AsyncClient( + self._async_client = httpx2.AsyncClient( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -122,13 +122,13 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: ) return self._async_client - async def __aenter__(self) -> "Client": - """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" await self.get_async_httpx_client().__aenter__() return self async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) @@ -136,7 +136,7 @@ async def __aexit__(self, *args: Any, **kwargs: Any) -> None: class AuthenticatedClient: """A Client which has been authenticated for use on secured endpoints - The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: ``base_url``: The base URL for the API, all requests are made to a relative path to this URL @@ -145,14 +145,14 @@ class AuthenticatedClient: ``headers``: A dictionary of headers to be sent with every request ``timeout``: The maximum amount of a time a request can take. API functions will raise - httpx.TimeoutException if this is exceeded. + httpx2.TimeoutException if this is exceeded. ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, but can be set to False for testing purposes. ``follow_redirects``: Whether or not to follow redirects. Default value is False. - ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. Attributes: @@ -168,12 +168,12 @@ class AuthenticatedClient: _base_url: str = field(alias="base_url") _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") - _client: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) token: str prefix: str = "Bearer" @@ -195,27 +195,27 @@ def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": self._async_client.cookies.update(cookies) return evolve(self, cookies={**self._cookies, **cookies}) - def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": - """Get a new client matching this one with a new timeout (in seconds)""" + def with_timeout(self, timeout: httpx2.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" if self._client is not None: self._client.timeout = timeout if self._async_client is not None: self._async_client.timeout = timeout return evolve(self, timeout=timeout) - def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": - """Manually set the underlying httpx.Client + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._client = client return self - def get_httpx_client(self) -> httpx.Client: - """Get the underlying httpx.Client, constructing a new one if not previously set""" + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" if self._client is None: self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token - self._client = httpx.Client( + self._client = httpx2.Client( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -226,28 +226,28 @@ def get_httpx_client(self) -> httpx.Client: ) return self._client - def __enter__(self) -> "AuthenticatedClient": - """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" self.get_httpx_client().__enter__() return self def __exit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for internal httpx.Client (see httpx docs)""" + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" self.get_httpx_client().__exit__(*args, **kwargs) - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient": - """Manually the underlying httpx.AsyncClient + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._async_client = async_client return self - def get_async_httpx_client(self) -> httpx.AsyncClient: - """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" if self._async_client is None: self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token - self._async_client = httpx.AsyncClient( + self._async_client = httpx2.AsyncClient( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -258,11 +258,11 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: ) return self._async_client - async def __aenter__(self) -> "AuthenticatedClient": - """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" await self.get_async_httpx_client().__aenter__() return self async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/errors.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/errors.py new file mode 100644 index 000000000..5f92e76ac --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/errors.py @@ -0,0 +1,16 @@ +"""Contains shared errors types that can be raised from API functions""" + + +class UnexpectedStatus(Exception): + """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" + + def __init__(self, status_code: int, content: bytes): + self.status_code = status_code + self.content = content + + super().__init__( + f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" + ) + + +__all__ = ["UnexpectedStatus"] diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/__init__.py similarity index 89% rename from end_to_end_tests/golden-record/my_test_api_client/models/__init__.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/__init__.py index f354c31c7..c62e4cfa6 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/__init__.py @@ -7,6 +7,8 @@ from .a_model_with_properties_reference_that_are_not_object import AModelWithPropertiesReferenceThatAreNotObject from .all_of_has_properties_but_no_type import AllOfHasPropertiesButNoType from .all_of_has_properties_but_no_type_type_enum import AllOfHasPropertiesButNoTypeTypeEnum +from .all_of_required_base import AllOfRequiredBase +from .all_of_required_derived import AllOfRequiredDerived from .all_of_sub_model import AllOfSubModel from .all_of_sub_model_type_enum import AllOfSubModelTypeEnum from .an_all_of_enum import AnAllOfEnum @@ -72,9 +74,11 @@ from .model_with_recursive_ref_in_additional_properties import ModelWithRecursiveRefInAdditionalProperties from .model_with_union_property import ModelWithUnionProperty from .model_with_union_property_inlined import ModelWithUnionPropertyInlined -from .model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0 -from .model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1 +from .model_with_union_property_inlined_apples import ModelWithUnionPropertyInlinedApples +from .model_with_union_property_inlined_bananas import ModelWithUnionPropertyInlinedBananas from .none import None_ +from .octet_stream_tests_octet_stream_post_response_200 import OctetStreamTestsOctetStreamPostResponse200 +from .optional_body_body import OptionalBodyBody from .post_bodies_multiple_data_body import PostBodiesMultipleDataBody from .post_bodies_multiple_files_body import PostBodiesMultipleFilesBody from .post_bodies_multiple_json_body import PostBodiesMultipleJsonBody @@ -85,6 +89,9 @@ from .post_responses_unions_simple_before_complex_response_200a_type_1 import ( PostResponsesUnionsSimpleBeforeComplexResponse200AType1, ) +from .status_code_patterns_response_2xx import StatusCodePatternsResponse2XX +from .status_code_patterns_response_2xx_status import StatusCodePatternsResponse2XXStatus +from .status_code_patterns_response_4xx import StatusCodePatternsResponse4XX from .test_inline_objects_body import TestInlineObjectsBody from .test_inline_objects_response_200 import TestInlineObjectsResponse200 from .validation_error import ValidationError @@ -95,6 +102,8 @@ "AFormData", "AllOfHasPropertiesButNoType", "AllOfHasPropertiesButNoTypeTypeEnum", + "AllOfRequiredBase", + "AllOfRequiredDerived", "AllOfSubModel", "AllOfSubModelTypeEnum", "AModel", @@ -154,9 +163,11 @@ "ModelWithRecursiveRefInAdditionalProperties", "ModelWithUnionProperty", "ModelWithUnionPropertyInlined", - "ModelWithUnionPropertyInlinedFruitType0", - "ModelWithUnionPropertyInlinedFruitType1", + "ModelWithUnionPropertyInlinedApples", + "ModelWithUnionPropertyInlinedBananas", "None_", + "OctetStreamTestsOctetStreamPostResponse200", + "OptionalBodyBody", "PostBodiesMultipleDataBody", "PostBodiesMultipleFilesBody", "PostBodiesMultipleJsonBody", @@ -165,6 +176,9 @@ "PostNamingPropertyConflictWithImportResponse200", "PostResponsesUnionsSimpleBeforeComplexResponse200", "PostResponsesUnionsSimpleBeforeComplexResponse200AType1", + "StatusCodePatternsResponse2XX", + "StatusCodePatternsResponse2XXStatus", + "StatusCodePatternsResponse4XX", "TestInlineObjectsBody", "TestInlineObjectsResponse200", "ValidationError", diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_1.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_discriminated_union_type_1.py similarity index 84% rename from end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_1.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_discriminated_union_type_1.py index efdee8d2b..635661b7b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_1.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_discriminated_union_type_1.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,10 +15,10 @@ class ADiscriminatedUnionType1: """ Attributes: - model_type (Union[Unset, str]): + model_type (str | Unset): """ - model_type: Union[Unset, str] = UNSET + model_type: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -30,8 +33,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) model_type = d.pop("modelType", UNSET) a_discriminated_union_type_1 = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_2.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_discriminated_union_type_2.py similarity index 84% rename from end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_2.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_discriminated_union_type_2.py index 02f4870e6..0797af899 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_discriminated_union_type_2.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_discriminated_union_type_2.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,10 +15,10 @@ class ADiscriminatedUnionType2: """ Attributes: - model_type (Union[Unset, str]): + model_type (str | Unset): """ - model_type: Union[Unset, str] = UNSET + model_type: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -30,8 +33,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) model_type = d.pop("modelType", UNSET) a_discriminated_union_type_2 = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_form_data.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_form_data.py similarity index 85% rename from end_to_end_tests/golden-record/my_test_api_client/models/a_form_data.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_form_data.py index 12c57c3bc..257054eb2 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_form_data.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_form_data.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -13,11 +16,11 @@ class AFormData: """ Attributes: an_required_field (str): - an_optional_field (Union[Unset, str]): + an_optional_field (str | Unset): """ an_required_field: str - an_optional_field: Union[Unset, str] = UNSET + an_optional_field: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -38,8 +41,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) an_required_field = d.pop("an_required_field") an_optional_field = d.pop("an_optional_field", UNSET) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_model.py similarity index 69% rename from end_to_end_tests/golden-record/my_test_api_client/models/a_model.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_model.py index 5a81e6365..4ca83858f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_model.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import datetime -from typing import TYPE_CHECKING, Any, TypeVar, Union, cast +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast from uuid import UUID from attrs import define as _attrs_define -from dateutil.parser import isoparse from ..models.an_all_of_enum import AnAllOfEnum from ..models.an_enum import AnEnum @@ -25,62 +27,62 @@ class AModel: Attributes: an_enum_value (AnEnum): For testing Enums in all the ways they can be used an_allof_enum_with_overridden_default (AnAllOfEnum): Default: AnAllOfEnum.OVERRIDDEN_DEFAULT. - a_camel_date_time (Union[datetime.date, datetime.datetime]): + a_camel_date_time (datetime.date | datetime.datetime): a_date (datetime.date): - a_nullable_date (Union[None, datetime.date]): + a_nullable_date (datetime.date | None): a_uuid (UUID): - a_nullable_uuid (Union[None, UUID]): Default: UUID('07EF8B4D-AA09-4FFA-898D-C710796AFF41'). - required_nullable (Union[None, str]): + a_nullable_uuid (None | UUID): Default: UUID('07EF8B4D-AA09-4FFA-898D-C710796AFF41'). + required_nullable (None | str): required_not_nullable (str): - one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Any]): - nullable_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', None]): + one_of_models (Any | FreeFormModel | ModelWithUnionProperty): + nullable_one_of_models (FreeFormModel | ModelWithUnionProperty | None): model (ModelWithUnionProperty): - nullable_model (Union['ModelWithUnionProperty', None]): - any_value (Union[Unset, Any]): Default: 'default'. - an_optional_allof_enum (Union[Unset, AnAllOfEnum]): - nested_list_of_enums (Union[Unset, list[list[DifferentEnum]]]): - a_not_required_date (Union[Unset, datetime.date]): - a_not_required_uuid (Union[Unset, UUID]): - attr_1_leading_digit (Union[Unset, str]): - attr_leading_underscore (Union[Unset, str]): - not_required_nullable (Union[None, Unset, str]): - not_required_not_nullable (Union[Unset, str]): - not_required_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Unset]): - not_required_nullable_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', None, Unset, str]): - not_required_model (Union[Unset, ModelWithUnionProperty]): - not_required_nullable_model (Union['ModelWithUnionProperty', None, Unset]): + nullable_model (ModelWithUnionProperty | None): + any_value (Any | Unset): Default: 'default'. + an_optional_allof_enum (AnAllOfEnum | Unset): + nested_list_of_enums (list[list[DifferentEnum]] | Unset): + a_not_required_date (datetime.date | Unset): + a_not_required_uuid (UUID | Unset): + attr_1_leading_digit (str | Unset): + attr_leading_underscore (str | Unset): + not_required_nullable (None | str | Unset): + not_required_not_nullable (str | Unset): + not_required_one_of_models (FreeFormModel | ModelWithUnionProperty | Unset): + not_required_nullable_one_of_models (FreeFormModel | ModelWithUnionProperty | None | str | Unset): + not_required_model (ModelWithUnionProperty | Unset): + not_required_nullable_model (ModelWithUnionProperty | None | Unset): """ an_enum_value: AnEnum - a_camel_date_time: Union[datetime.date, datetime.datetime] + a_camel_date_time: datetime.date | datetime.datetime a_date: datetime.date - a_nullable_date: Union[None, datetime.date] + a_nullable_date: datetime.date | None a_uuid: UUID - required_nullable: Union[None, str] + required_nullable: None | str required_not_nullable: str - one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Any] - nullable_one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", None] - model: "ModelWithUnionProperty" - nullable_model: Union["ModelWithUnionProperty", None] + one_of_models: Any | FreeFormModel | ModelWithUnionProperty + nullable_one_of_models: FreeFormModel | ModelWithUnionProperty | None + model: ModelWithUnionProperty + nullable_model: ModelWithUnionProperty | None an_allof_enum_with_overridden_default: AnAllOfEnum = AnAllOfEnum.OVERRIDDEN_DEFAULT - a_nullable_uuid: Union[None, UUID] = UUID("07EF8B4D-AA09-4FFA-898D-C710796AFF41") - any_value: Union[Unset, Any] = "default" - an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET - nested_list_of_enums: Union[Unset, list[list[DifferentEnum]]] = UNSET - a_not_required_date: Union[Unset, datetime.date] = UNSET - a_not_required_uuid: Union[Unset, UUID] = UNSET - attr_1_leading_digit: Union[Unset, str] = UNSET - attr_leading_underscore: Union[Unset, str] = UNSET - not_required_nullable: Union[None, Unset, str] = UNSET - not_required_not_nullable: Union[Unset, str] = UNSET - not_required_one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Unset] = UNSET - not_required_nullable_one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", None, Unset, str] = UNSET - not_required_model: Union[Unset, "ModelWithUnionProperty"] = UNSET - not_required_nullable_model: Union["ModelWithUnionProperty", None, Unset] = UNSET + a_nullable_uuid: None | UUID = UUID("07EF8B4D-AA09-4FFA-898D-C710796AFF41") + any_value: Any | Unset = "default" + an_optional_allof_enum: AnAllOfEnum | Unset = UNSET + nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET + a_not_required_date: datetime.date | Unset = UNSET + a_not_required_uuid: UUID | Unset = UNSET + attr_1_leading_digit: str | Unset = UNSET + attr_leading_underscore: str | Unset = UNSET + not_required_nullable: None | str | Unset = UNSET + not_required_not_nullable: str | Unset = UNSET + not_required_one_of_models: FreeFormModel | ModelWithUnionProperty | Unset = UNSET + not_required_nullable_one_of_models: FreeFormModel | ModelWithUnionProperty | None | str | Unset = UNSET + not_required_model: ModelWithUnionProperty | Unset = UNSET + not_required_nullable_model: ModelWithUnionProperty | None | Unset = UNSET def to_dict(self) -> dict[str, Any]: - from ..models.free_form_model import FreeFormModel - from ..models.model_with_union_property import ModelWithUnionProperty + from ..models.free_form_model import FreeFormModel # noqa: PLC0415 + from ..models.model_with_union_property import ModelWithUnionProperty # noqa: PLC0415 an_enum_value = self.an_enum_value.value @@ -94,7 +96,7 @@ def to_dict(self) -> dict[str, Any]: a_date = self.a_date.isoformat() - a_nullable_date: Union[None, str] + a_nullable_date: None | str if isinstance(self.a_nullable_date, datetime.date): a_nullable_date = self.a_nullable_date.isoformat() else: @@ -102,18 +104,18 @@ def to_dict(self) -> dict[str, Any]: a_uuid = str(self.a_uuid) - a_nullable_uuid: Union[None, str] + a_nullable_uuid: None | str if isinstance(self.a_nullable_uuid, UUID): a_nullable_uuid = str(self.a_nullable_uuid) else: a_nullable_uuid = self.a_nullable_uuid - required_nullable: Union[None, str] + required_nullable: None | str required_nullable = self.required_nullable required_not_nullable = self.required_not_nullable - one_of_models: Union[Any, dict[str, Any]] + one_of_models: Any | dict[str, Any] if isinstance(self.one_of_models, FreeFormModel): one_of_models = self.one_of_models.to_dict() elif isinstance(self.one_of_models, ModelWithUnionProperty): @@ -121,7 +123,7 @@ def to_dict(self) -> dict[str, Any]: else: one_of_models = self.one_of_models - nullable_one_of_models: Union[None, dict[str, Any]] + nullable_one_of_models: dict[str, Any] | None if isinstance(self.nullable_one_of_models, FreeFormModel): nullable_one_of_models = self.nullable_one_of_models.to_dict() elif isinstance(self.nullable_one_of_models, ModelWithUnionProperty): @@ -131,7 +133,7 @@ def to_dict(self) -> dict[str, Any]: model = self.model.to_dict() - nullable_model: Union[None, dict[str, Any]] + nullable_model: dict[str, Any] | None if isinstance(self.nullable_model, ModelWithUnionProperty): nullable_model = self.nullable_model.to_dict() else: @@ -139,11 +141,11 @@ def to_dict(self) -> dict[str, Any]: any_value = self.any_value - an_optional_allof_enum: Union[Unset, str] = UNSET + an_optional_allof_enum: str | Unset = UNSET if not isinstance(self.an_optional_allof_enum, Unset): an_optional_allof_enum = self.an_optional_allof_enum.value - nested_list_of_enums: Union[Unset, list[list[str]]] = UNSET + nested_list_of_enums: list[list[str]] | Unset = UNSET if not isinstance(self.nested_list_of_enums, Unset): nested_list_of_enums = [] for nested_list_of_enums_item_data in self.nested_list_of_enums: @@ -154,11 +156,11 @@ def to_dict(self) -> dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) - a_not_required_date: Union[Unset, str] = UNSET + a_not_required_date: str | Unset = UNSET if not isinstance(self.a_not_required_date, Unset): a_not_required_date = self.a_not_required_date.isoformat() - a_not_required_uuid: Union[Unset, str] = UNSET + a_not_required_uuid: str | Unset = UNSET if not isinstance(self.a_not_required_uuid, Unset): a_not_required_uuid = str(self.a_not_required_uuid) @@ -166,7 +168,7 @@ def to_dict(self) -> dict[str, Any]: attr_leading_underscore = self.attr_leading_underscore - not_required_nullable: Union[None, Unset, str] + not_required_nullable: None | str | Unset if isinstance(self.not_required_nullable, Unset): not_required_nullable = UNSET else: @@ -174,7 +176,7 @@ def to_dict(self) -> dict[str, Any]: not_required_not_nullable = self.not_required_not_nullable - not_required_one_of_models: Union[Unset, dict[str, Any]] + not_required_one_of_models: dict[str, Any] | Unset if isinstance(self.not_required_one_of_models, Unset): not_required_one_of_models = UNSET elif isinstance(self.not_required_one_of_models, FreeFormModel): @@ -182,7 +184,7 @@ def to_dict(self) -> dict[str, Any]: else: not_required_one_of_models = self.not_required_one_of_models.to_dict() - not_required_nullable_one_of_models: Union[None, Unset, dict[str, Any], str] + not_required_nullable_one_of_models: dict[str, Any] | None | str | Unset if isinstance(self.not_required_nullable_one_of_models, Unset): not_required_nullable_one_of_models = UNSET elif isinstance(self.not_required_nullable_one_of_models, FreeFormModel): @@ -192,11 +194,11 @@ def to_dict(self) -> dict[str, Any]: else: not_required_nullable_one_of_models = self.not_required_nullable_one_of_models - not_required_model: Union[Unset, dict[str, Any]] = UNSET + not_required_model: dict[str, Any] | Unset = UNSET if not isinstance(self.not_required_model, Unset): not_required_model = self.not_required_model.to_dict() - not_required_nullable_model: Union[None, Unset, dict[str, Any]] + not_required_nullable_model: dict[str, Any] | None | Unset if isinstance(self.not_required_nullable_model, Unset): not_required_nullable_model = UNSET elif isinstance(self.not_required_nullable_model, ModelWithUnionProperty): @@ -205,6 +207,7 @@ def to_dict(self) -> dict[str, Any]: not_required_nullable_model = self.not_required_nullable_model field_dict: dict[str, Any] = {} + field_dict.update( { "an_enum_value": an_enum_value, @@ -252,52 +255,52 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - from ..models.free_form_model import FreeFormModel - from ..models.model_with_union_property import ModelWithUnionProperty + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.free_form_model import FreeFormModel # noqa: PLC0415 + from ..models.model_with_union_property import ModelWithUnionProperty # noqa: PLC0415 - d = src_dict.copy() + d = dict(src_dict) an_enum_value = AnEnum(d.pop("an_enum_value")) an_allof_enum_with_overridden_default = AnAllOfEnum(d.pop("an_allof_enum_with_overridden_default")) - def _parse_a_camel_date_time(data: object) -> Union[datetime.date, datetime.datetime]: + def _parse_a_camel_date_time(data: object) -> datetime.date | datetime.datetime: try: if not isinstance(data, str): raise TypeError() - a_camel_date_time_type_0 = isoparse(data) + a_camel_date_time_type_0 = datetime.datetime.fromisoformat(data) return a_camel_date_time_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass if not isinstance(data, str): raise TypeError() - a_camel_date_time_type_1 = isoparse(data).date() + a_camel_date_time_type_1 = datetime.date.fromisoformat(data) return a_camel_date_time_type_1 a_camel_date_time = _parse_a_camel_date_time(d.pop("aCamelDateTime")) - a_date = isoparse(d.pop("a_date")).date() + a_date = datetime.date.fromisoformat(d.pop("a_date")) - def _parse_a_nullable_date(data: object) -> Union[None, datetime.date]: + def _parse_a_nullable_date(data: object) -> datetime.date | None: if data is None: return data try: if not isinstance(data, str): raise TypeError() - a_nullable_date_type_0 = isoparse(data).date() + a_nullable_date_type_0 = datetime.date.fromisoformat(data) return a_nullable_date_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union[None, datetime.date], data) + return cast(datetime.date | None, data) a_nullable_date = _parse_a_nullable_date(d.pop("a_nullable_date")) a_uuid = UUID(d.pop("a_uuid")) - def _parse_a_nullable_uuid(data: object) -> Union[None, UUID]: + def _parse_a_nullable_uuid(data: object) -> None | UUID: if data is None: return data try: @@ -306,29 +309,29 @@ def _parse_a_nullable_uuid(data: object) -> Union[None, UUID]: a_nullable_uuid_type_0 = UUID(data) return a_nullable_uuid_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union[None, UUID], data) + return cast(None | UUID, data) a_nullable_uuid = _parse_a_nullable_uuid(d.pop("a_nullable_uuid")) - def _parse_required_nullable(data: object) -> Union[None, str]: + def _parse_required_nullable(data: object) -> None | str: if data is None: return data - return cast(Union[None, str], data) + return cast(None | str, data) required_nullable = _parse_required_nullable(d.pop("required_nullable")) required_not_nullable = d.pop("required_not_nullable") - def _parse_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", Any]: + def _parse_one_of_models(data: object) -> Any | FreeFormModel | ModelWithUnionProperty: try: if not isinstance(data, dict): raise TypeError() one_of_models_type_0 = FreeFormModel.from_dict(data) return one_of_models_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass try: if not isinstance(data, dict): @@ -336,13 +339,13 @@ def _parse_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnion one_of_models_type_1 = ModelWithUnionProperty.from_dict(data) return one_of_models_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["FreeFormModel", "ModelWithUnionProperty", Any], data) + return cast(Any | FreeFormModel | ModelWithUnionProperty, data) one_of_models = _parse_one_of_models(d.pop("one_of_models")) - def _parse_nullable_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", None]: + def _parse_nullable_one_of_models(data: object) -> FreeFormModel | ModelWithUnionProperty | None: if data is None: return data try: @@ -351,7 +354,7 @@ def _parse_nullable_one_of_models(data: object) -> Union["FreeFormModel", "Model nullable_one_of_models_type_0 = FreeFormModel.from_dict(data) return nullable_one_of_models_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass try: if not isinstance(data, dict): @@ -359,15 +362,15 @@ def _parse_nullable_one_of_models(data: object) -> Union["FreeFormModel", "Model nullable_one_of_models_type_1 = ModelWithUnionProperty.from_dict(data) return nullable_one_of_models_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["FreeFormModel", "ModelWithUnionProperty", None], data) + return cast(FreeFormModel | ModelWithUnionProperty | None, data) nullable_one_of_models = _parse_nullable_one_of_models(d.pop("nullable_one_of_models")) model = ModelWithUnionProperty.from_dict(d.pop("model")) - def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None]: + def _parse_nullable_model(data: object) -> ModelWithUnionProperty | None: if data is None: return data try: @@ -376,42 +379,44 @@ def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None] nullable_model_type_1 = ModelWithUnionProperty.from_dict(data) return nullable_model_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["ModelWithUnionProperty", None], data) + return cast(ModelWithUnionProperty | None, data) nullable_model = _parse_nullable_model(d.pop("nullable_model")) any_value = d.pop("any_value", UNSET) _an_optional_allof_enum = d.pop("an_optional_allof_enum", UNSET) - an_optional_allof_enum: Union[Unset, AnAllOfEnum] + an_optional_allof_enum: AnAllOfEnum | Unset if isinstance(_an_optional_allof_enum, Unset): an_optional_allof_enum = UNSET else: an_optional_allof_enum = AnAllOfEnum(_an_optional_allof_enum) - nested_list_of_enums = [] _nested_list_of_enums = d.pop("nested_list_of_enums", UNSET) - for nested_list_of_enums_item_data in _nested_list_of_enums or []: - nested_list_of_enums_item = [] - _nested_list_of_enums_item = nested_list_of_enums_item_data - for nested_list_of_enums_item_item_data in _nested_list_of_enums_item: - nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) + nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET + if _nested_list_of_enums is not UNSET: + nested_list_of_enums = [] + for nested_list_of_enums_item_data in _nested_list_of_enums: + nested_list_of_enums_item = [] + _nested_list_of_enums_item = nested_list_of_enums_item_data + for nested_list_of_enums_item_item_data in _nested_list_of_enums_item: + nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) - nested_list_of_enums_item.append(nested_list_of_enums_item_item) + nested_list_of_enums_item.append(nested_list_of_enums_item_item) - nested_list_of_enums.append(nested_list_of_enums_item) + nested_list_of_enums.append(nested_list_of_enums_item) _a_not_required_date = d.pop("a_not_required_date", UNSET) - a_not_required_date: Union[Unset, datetime.date] + a_not_required_date: datetime.date | Unset if isinstance(_a_not_required_date, Unset): a_not_required_date = UNSET else: - a_not_required_date = isoparse(_a_not_required_date).date() + a_not_required_date = datetime.date.fromisoformat(_a_not_required_date) _a_not_required_uuid = d.pop("a_not_required_uuid", UNSET) - a_not_required_uuid: Union[Unset, UUID] + a_not_required_uuid: UUID | Unset if isinstance(_a_not_required_uuid, Unset): a_not_required_uuid = UNSET else: @@ -421,18 +426,18 @@ def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None] attr_leading_underscore = d.pop("_leading_underscore", UNSET) - def _parse_not_required_nullable(data: object) -> Union[None, Unset, str]: + def _parse_not_required_nullable(data: object) -> None | str | Unset: if data is None: return data if isinstance(data, Unset): return data - return cast(Union[None, Unset, str], data) + return cast(None | str | Unset, data) not_required_nullable = _parse_not_required_nullable(d.pop("not_required_nullable", UNSET)) not_required_not_nullable = d.pop("not_required_not_nullable", UNSET) - def _parse_not_required_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", Unset]: + def _parse_not_required_one_of_models(data: object) -> FreeFormModel | ModelWithUnionProperty | Unset: if isinstance(data, Unset): return data try: @@ -441,7 +446,7 @@ def _parse_not_required_one_of_models(data: object) -> Union["FreeFormModel", "M not_required_one_of_models_type_0 = FreeFormModel.from_dict(data) return not_required_one_of_models_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass if not isinstance(data, dict): raise TypeError() @@ -453,7 +458,7 @@ def _parse_not_required_one_of_models(data: object) -> Union["FreeFormModel", "M def _parse_not_required_nullable_one_of_models( data: object, - ) -> Union["FreeFormModel", "ModelWithUnionProperty", None, Unset, str]: + ) -> FreeFormModel | ModelWithUnionProperty | None | str | Unset: if data is None: return data if isinstance(data, Unset): @@ -464,7 +469,7 @@ def _parse_not_required_nullable_one_of_models( not_required_nullable_one_of_models_type_0 = FreeFormModel.from_dict(data) return not_required_nullable_one_of_models_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass try: if not isinstance(data, dict): @@ -472,22 +477,22 @@ def _parse_not_required_nullable_one_of_models( not_required_nullable_one_of_models_type_1 = ModelWithUnionProperty.from_dict(data) return not_required_nullable_one_of_models_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["FreeFormModel", "ModelWithUnionProperty", None, Unset, str], data) + return cast(FreeFormModel | ModelWithUnionProperty | None | str | Unset, data) not_required_nullable_one_of_models = _parse_not_required_nullable_one_of_models( d.pop("not_required_nullable_one_of_models", UNSET) ) _not_required_model = d.pop("not_required_model", UNSET) - not_required_model: Union[Unset, ModelWithUnionProperty] + not_required_model: ModelWithUnionProperty | Unset if isinstance(_not_required_model, Unset): not_required_model = UNSET else: not_required_model = ModelWithUnionProperty.from_dict(_not_required_model) - def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionProperty", None, Unset]: + def _parse_not_required_nullable_model(data: object) -> ModelWithUnionProperty | None | Unset: if data is None: return data if isinstance(data, Unset): @@ -498,9 +503,9 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro not_required_nullable_model_type_1 = ModelWithUnionProperty.from_dict(data) return not_required_nullable_model_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["ModelWithUnionProperty", None, Unset], data) + return cast(ModelWithUnionProperty | None | Unset, data) not_required_nullable_model = _parse_not_required_nullable_model(d.pop("not_required_nullable_model", UNSET)) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py similarity index 94% rename from end_to_end_tests/golden-record/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py index c1a905195..4c31bf01b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/a_model_with_properties_reference_that_are_not_object.py @@ -1,10 +1,12 @@ +from __future__ import annotations + import datetime +from collections.abc import Mapping from io import BytesIO from typing import Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field -from dateutil.parser import isoparse from ..models.an_enum import AnEnum from ..types import File @@ -213,8 +215,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) enum_properties_ref = [] _enum_properties_ref = d.pop("enum_properties_ref") for componentsschemas_an_other_array_of_enum_item_data in _enum_properties_ref: @@ -227,16 +229,16 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: date_properties_ref = [] _date_properties_ref = d.pop("date_properties_ref") for componentsschemas_an_other_array_of_date_item_data in _date_properties_ref: - componentsschemas_an_other_array_of_date_item = isoparse( + componentsschemas_an_other_array_of_date_item = datetime.date.fromisoformat( componentsschemas_an_other_array_of_date_item_data - ).date() + ) date_properties_ref.append(componentsschemas_an_other_array_of_date_item) datetime_properties_ref = [] _datetime_properties_ref = d.pop("datetime_properties_ref") for componentsschemas_an_other_array_of_date_time_item_data in _datetime_properties_ref: - componentsschemas_an_other_array_of_date_time_item = isoparse( + componentsschemas_an_other_array_of_date_time_item = datetime.datetime.fromisoformat( componentsschemas_an_other_array_of_date_time_item_data ) @@ -273,14 +275,18 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: date_properties = [] _date_properties = d.pop("date_properties") for componentsschemas_an_array_of_date_item_data in _date_properties: - componentsschemas_an_array_of_date_item = isoparse(componentsschemas_an_array_of_date_item_data).date() + componentsschemas_an_array_of_date_item = datetime.date.fromisoformat( + componentsschemas_an_array_of_date_item_data + ) date_properties.append(componentsschemas_an_array_of_date_item) datetime_properties = [] _datetime_properties = d.pop("datetime_properties") for componentsschemas_an_array_of_date_time_item_data in _datetime_properties: - componentsschemas_an_array_of_date_time_item = isoparse(componentsschemas_an_array_of_date_time_item_data) + componentsschemas_an_array_of_date_time_item = datetime.datetime.fromisoformat( + componentsschemas_an_array_of_date_time_item_data + ) datetime_properties.append(componentsschemas_an_array_of_date_time_item) @@ -307,9 +313,9 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: str_property_ref = d.pop("str_property_ref") - date_property_ref = isoparse(d.pop("date_property_ref")).date() + date_property_ref = datetime.date.fromisoformat(d.pop("date_property_ref")) - datetime_property_ref = isoparse(d.pop("datetime_property_ref")) + datetime_property_ref = datetime.datetime.fromisoformat(d.pop("datetime_property_ref")) int32_property_ref = d.pop("int32_property_ref") diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_has_properties_but_no_type.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_has_properties_but_no_type.py similarity index 79% rename from end_to_end_tests/golden-record/my_test_api_client/models/all_of_has_properties_but_no_type.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_has_properties_but_no_type.py index 6a39f4951..3295dcf26 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_has_properties_but_no_type.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_has_properties_but_no_type.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -13,14 +16,14 @@ class AllOfHasPropertiesButNoType: """ Attributes: - a_sub_property (Union[Unset, str]): - type_ (Union[Unset, str]): - type_enum (Union[Unset, AllOfHasPropertiesButNoTypeTypeEnum]): + a_sub_property (str | Unset): + type_ (str | Unset): + type_enum (AllOfHasPropertiesButNoTypeTypeEnum | Unset): """ - a_sub_property: Union[Unset, str] = UNSET - type_: Union[Unset, str] = UNSET - type_enum: Union[Unset, AllOfHasPropertiesButNoTypeTypeEnum] = UNSET + a_sub_property: str | Unset = UNSET + type_: str | Unset = UNSET + type_enum: AllOfHasPropertiesButNoTypeTypeEnum | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -28,7 +31,7 @@ def to_dict(self) -> dict[str, Any]: type_ = self.type_ - type_enum: Union[Unset, int] = UNSET + type_enum: int | Unset = UNSET if not isinstance(self.type_enum, Unset): type_enum = self.type_enum.value @@ -45,14 +48,14 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) a_sub_property = d.pop("a_sub_property", UNSET) type_ = d.pop("type", UNSET) _type_enum = d.pop("type_enum", UNSET) - type_enum: Union[Unset, AllOfHasPropertiesButNoTypeTypeEnum] + type_enum: AllOfHasPropertiesButNoTypeTypeEnum | Unset if isinstance(_type_enum, Unset): type_enum = UNSET else: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_has_properties_but_no_type_type_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_has_properties_but_no_type_type_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/all_of_has_properties_but_no_type_type_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_has_properties_but_no_type_type_enum.py diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_required_base.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_required_base.py new file mode 100644 index 000000000..b06f45c30 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_required_base.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AllOfRequiredBase") + + +@_attrs_define +class AllOfRequiredBase: + """ + Attributes: + bar (str | Unset): The bar property + baz (str | Unset): The baz property + """ + + bar: str | Unset = UNSET + baz: str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + bar = self.bar + + baz = self.baz + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if bar is not UNSET: + field_dict["bar"] = bar + if baz is not UNSET: + field_dict["baz"] = baz + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + bar = d.pop("bar", UNSET) + + baz = d.pop("baz", UNSET) + + all_of_required_base = cls( + bar=bar, + baz=baz, + ) + + all_of_required_base.additional_properties = d + return all_of_required_base + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_required_derived.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_required_derived.py new file mode 100644 index 000000000..505334d6b --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_required_derived.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AllOfRequiredDerived") + + +@_attrs_define +class AllOfRequiredDerived: + """ + Attributes: + bar (str): The bar property + baz (str | Unset): The baz property + """ + + bar: str + baz: str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + bar = self.bar + + baz = self.baz + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "bar": bar, + } + ) + if baz is not UNSET: + field_dict["baz"] = baz + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + bar = d.pop("bar") + + baz = d.pop("baz", UNSET) + + all_of_required_derived = cls( + bar=bar, + baz=baz, + ) + + all_of_required_derived.additional_properties = d + return all_of_required_derived + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_sub_model.py similarity index 79% rename from end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_sub_model.py index dfb672ce1..65b6f97c6 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_sub_model.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -13,14 +16,14 @@ class AllOfSubModel: """ Attributes: - a_sub_property (Union[Unset, str]): - type_ (Union[Unset, str]): - type_enum (Union[Unset, AllOfSubModelTypeEnum]): + a_sub_property (str | Unset): + type_ (str | Unset): + type_enum (AllOfSubModelTypeEnum | Unset): """ - a_sub_property: Union[Unset, str] = UNSET - type_: Union[Unset, str] = UNSET - type_enum: Union[Unset, AllOfSubModelTypeEnum] = UNSET + a_sub_property: str | Unset = UNSET + type_: str | Unset = UNSET + type_enum: AllOfSubModelTypeEnum | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -28,7 +31,7 @@ def to_dict(self) -> dict[str, Any]: type_ = self.type_ - type_enum: Union[Unset, int] = UNSET + type_enum: int | Unset = UNSET if not isinstance(self.type_enum, Unset): type_enum = self.type_enum.value @@ -45,14 +48,14 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) a_sub_property = d.pop("a_sub_property", UNSET) type_ = d.pop("type", UNSET) _type_enum = d.pop("type_enum", UNSET) - type_enum: Union[Unset, AllOfSubModelTypeEnum] + type_enum: AllOfSubModelTypeEnum | Unset if isinstance(_type_enum, Unset): type_enum = UNSET else: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model_type_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_sub_model_type_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/all_of_sub_model_type_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/all_of_sub_model_type_enum.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_all_of_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_all_of_enum.py similarity index 76% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_all_of_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_all_of_enum.py index 3aef48f8f..1439e1781 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_all_of_enum.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_all_of_enum.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class AnAllOfEnum(str, Enum): +class AnAllOfEnum(StrEnum): A_DEFAULT = "a_default" BAR = "bar" FOO = "foo" diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py similarity index 66% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py index 02e9cfa4b..77453bf2c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_a_item.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -16,14 +19,14 @@ class AnArrayWithACircularRefInItemsObjectAItem: """ Attributes: - circular (Union[Unset, list['AnArrayWithACircularRefInItemsObjectBItem']]): + circular (list[AnArrayWithACircularRefInItemsObjectBItem] | Unset): """ - circular: Union[Unset, list["AnArrayWithACircularRefInItemsObjectBItem"]] = UNSET + circular: list[AnArrayWithACircularRefInItemsObjectBItem] | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - circular: Union[Unset, list[dict[str, Any]]] = UNSET + circular: list[dict[str, Any]] | Unset = UNSET if not isinstance(self.circular, Unset): circular = [] for componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data in self.circular: @@ -41,22 +44,24 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.an_array_with_a_circular_ref_in_items_object_b_item import ( - AnArrayWithACircularRefInItemsObjectBItem, + AnArrayWithACircularRefInItemsObjectBItem, # noqa: PLC0415 ) - d = src_dict.copy() - circular = [] + d = dict(src_dict) _circular = d.pop("circular", UNSET) - for componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data in _circular or []: - componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item = ( - AnArrayWithACircularRefInItemsObjectBItem.from_dict( - componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data + circular: list[AnArrayWithACircularRefInItemsObjectBItem] | Unset = UNSET + if _circular is not UNSET: + circular = [] + for componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data in _circular: + componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item = ( + AnArrayWithACircularRefInItemsObjectBItem.from_dict( + componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data + ) ) - ) - circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item) + circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item) an_array_with_a_circular_ref_in_items_object_a_item = cls( circular=circular, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py similarity index 85% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py index c72c0160a..8a90d0eb3 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_a_item.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define @@ -14,13 +17,12 @@ @_attrs_define class AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem: - """ """ - - additional_properties: dict[str, list["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem"]] = ( + additional_properties: dict[str, list[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem]] = ( _attrs_field(init=False, factory=dict) ) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = [] @@ -35,12 +37,12 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.an_array_with_a_circular_ref_in_items_object_additional_properties_b_item import ( - AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem, + AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem, # noqa: PLC0415 ) - d = src_dict.copy() + d = dict(src_dict) an_array_with_a_circular_ref_in_items_object_additional_properties_a_item = cls() additional_properties = {} @@ -71,12 +73,10 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def additional_keys(self) -> list[str]: return list(self.additional_properties.keys()) - def __getitem__(self, key: str) -> list["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem"]: + def __getitem__(self, key: str) -> list[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem]: return self.additional_properties[key] - def __setitem__( - self, key: str, value: list["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem"] - ) -> None: + def __setitem__(self, key: str, value: list[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem]) -> None: self.additional_properties[key] = value def __delitem__(self, key: str) -> None: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py similarity index 85% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py index 7ffb50a16..e2b3e7ef5 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_additional_properties_b_item.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define @@ -14,13 +17,12 @@ @_attrs_define class AnArrayWithACircularRefInItemsObjectAdditionalPropertiesBItem: - """ """ - - additional_properties: dict[str, list["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem"]] = ( + additional_properties: dict[str, list[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem]] = ( _attrs_field(init=False, factory=dict) ) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = [] @@ -35,12 +37,12 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.an_array_with_a_circular_ref_in_items_object_additional_properties_a_item import ( - AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem, + AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem, # noqa: PLC0415 ) - d = src_dict.copy() + d = dict(src_dict) an_array_with_a_circular_ref_in_items_object_additional_properties_b_item = cls() additional_properties = {} @@ -71,12 +73,10 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def additional_keys(self) -> list[str]: return list(self.additional_properties.keys()) - def __getitem__(self, key: str) -> list["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem"]: + def __getitem__(self, key: str) -> list[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem]: return self.additional_properties[key] - def __setitem__( - self, key: str, value: list["AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem"] - ) -> None: + def __setitem__(self, key: str, value: list[AnArrayWithACircularRefInItemsObjectAdditionalPropertiesAItem]) -> None: self.additional_properties[key] = value def __delitem__(self, key: str) -> None: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py similarity index 66% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py index 6d5e83a65..e4039d6be 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_circular_ref_in_items_object_b_item.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -16,14 +19,14 @@ class AnArrayWithACircularRefInItemsObjectBItem: """ Attributes: - circular (Union[Unset, list['AnArrayWithACircularRefInItemsObjectAItem']]): + circular (list[AnArrayWithACircularRefInItemsObjectAItem] | Unset): """ - circular: Union[Unset, list["AnArrayWithACircularRefInItemsObjectAItem"]] = UNSET + circular: list[AnArrayWithACircularRefInItemsObjectAItem] | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - circular: Union[Unset, list[dict[str, Any]]] = UNSET + circular: list[dict[str, Any]] | Unset = UNSET if not isinstance(self.circular, Unset): circular = [] for componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data in self.circular: @@ -41,22 +44,24 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.an_array_with_a_circular_ref_in_items_object_a_item import ( - AnArrayWithACircularRefInItemsObjectAItem, + AnArrayWithACircularRefInItemsObjectAItem, # noqa: PLC0415 ) - d = src_dict.copy() - circular = [] + d = dict(src_dict) _circular = d.pop("circular", UNSET) - for componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data in _circular or []: - componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item = ( - AnArrayWithACircularRefInItemsObjectAItem.from_dict( - componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data + circular: list[AnArrayWithACircularRefInItemsObjectAItem] | Unset = UNSET + if _circular is not UNSET: + circular = [] + for componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data in _circular: + componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item = ( + AnArrayWithACircularRefInItemsObjectAItem.from_dict( + componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data + ) ) - ) - circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item) + circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item) an_array_with_a_circular_ref_in_items_object_b_item = cls( circular=circular, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py similarity index 84% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py index 14dc6ba26..12aba8360 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_additional_properties_item.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -8,13 +11,12 @@ @_attrs_define class AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem: - """ """ - - additional_properties: dict[str, list["AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem"]] = ( + additional_properties: dict[str, list[AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem]] = ( _attrs_field(init=False, factory=dict) ) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = [] @@ -27,8 +29,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) an_array_with_a_recursive_ref_in_items_object_additional_properties_item = cls() additional_properties = {} @@ -59,12 +61,10 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def additional_keys(self) -> list[str]: return list(self.additional_properties.keys()) - def __getitem__(self, key: str) -> list["AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem"]: + def __getitem__(self, key: str) -> list[AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem]: return self.additional_properties[key] - def __setitem__( - self, key: str, value: list["AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem"] - ) -> None: + def __setitem__(self, key: str, value: list[AnArrayWithARecursiveRefInItemsObjectAdditionalPropertiesItem]) -> None: self.additional_properties[key] = value def __delitem__(self, key: str) -> None: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py similarity index 65% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py index c8629e83d..581a9f622 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_array_with_a_recursive_ref_in_items_object_item.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,14 +15,14 @@ class AnArrayWithARecursiveRefInItemsObjectItem: """ Attributes: - recursive (Union[Unset, list['AnArrayWithARecursiveRefInItemsObjectItem']]): + recursive (list[AnArrayWithARecursiveRefInItemsObjectItem] | Unset): """ - recursive: Union[Unset, list["AnArrayWithARecursiveRefInItemsObjectItem"]] = UNSET + recursive: list[AnArrayWithARecursiveRefInItemsObjectItem] | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - recursive: Union[Unset, list[dict[str, Any]]] = UNSET + recursive: list[dict[str, Any]] | Unset = UNSET if not isinstance(self.recursive, Unset): recursive = [] for componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data in self.recursive: @@ -37,18 +40,20 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() - recursive = [] + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) _recursive = d.pop("recursive", UNSET) - for componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data in _recursive or []: - componentsschemas_an_array_with_a_recursive_ref_in_items_object_item = ( - AnArrayWithARecursiveRefInItemsObjectItem.from_dict( - componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data + recursive: list[AnArrayWithARecursiveRefInItemsObjectItem] | Unset = UNSET + if _recursive is not UNSET: + recursive = [] + for componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data in _recursive: + componentsschemas_an_array_with_a_recursive_ref_in_items_object_item = ( + AnArrayWithARecursiveRefInItemsObjectItem.from_dict( + componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data + ) ) - ) - recursive.append(componentsschemas_an_array_with_a_recursive_ref_in_items_object_item) + recursive.append(componentsschemas_an_array_with_a_recursive_ref_in_items_object_item) an_array_with_a_recursive_ref_in_items_object_item = cls( recursive=recursive, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_enum.py similarity index 73% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_enum.py index c266d0763..fe9c407d8 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_enum.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_enum.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class AnEnum(str, Enum): +class AnEnum(StrEnum): FIRST_VALUE = "FIRST_VALUE" SECOND_VALUE = "SECOND_VALUE" diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_enum_with_null.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_enum_with_null.py similarity index 69% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_enum_with_null.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_enum_with_null.py index b1d6611e0..002ee3667 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/an_enum_with_null.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_enum_with_null.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class AnEnumWithNull(str, Enum): +class AnEnumWithNull(StrEnum): FIRST_VALUE = "FIRST_VALUE" SECOND_VALUE = "SECOND_VALUE" diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/an_int_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_int_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/an_int_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/an_int_enum.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model.py similarity index 77% rename from end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model.py index df2d9b2cd..7c58d1a9c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -14,24 +17,24 @@ class AnotherAllOfSubModel: """ Attributes: - another_sub_property (Union[Unset, str]): - type_ (Union[Unset, AnotherAllOfSubModelType]): - type_enum (Union[Unset, AnotherAllOfSubModelTypeEnum]): + another_sub_property (str | Unset): + type_ (AnotherAllOfSubModelType | Unset): + type_enum (AnotherAllOfSubModelTypeEnum | Unset): """ - another_sub_property: Union[Unset, str] = UNSET - type_: Union[Unset, AnotherAllOfSubModelType] = UNSET - type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] = UNSET + another_sub_property: str | Unset = UNSET + type_: AnotherAllOfSubModelType | Unset = UNSET + type_enum: AnotherAllOfSubModelTypeEnum | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: another_sub_property = self.another_sub_property - type_: Union[Unset, str] = UNSET + type_: str | Unset = UNSET if not isinstance(self.type_, Unset): type_ = self.type_.value - type_enum: Union[Unset, int] = UNSET + type_enum: int | Unset = UNSET if not isinstance(self.type_enum, Unset): type_enum = self.type_enum.value @@ -48,19 +51,19 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) another_sub_property = d.pop("another_sub_property", UNSET) _type_ = d.pop("type", UNSET) - type_: Union[Unset, AnotherAllOfSubModelType] + type_: AnotherAllOfSubModelType | Unset if isinstance(_type_, Unset): type_ = UNSET else: type_ = AnotherAllOfSubModelType(_type_) _type_enum = d.pop("type_enum", UNSET) - type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] + type_enum: AnotherAllOfSubModelTypeEnum | Unset if isinstance(_type_enum, Unset): type_enum = UNSET else: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model_type.py similarity index 57% rename from end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model_type.py index b2e82aa7c..61d3a0c57 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model_type.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class AnotherAllOfSubModelType(str, Enum): +class AnotherAllOfSubModelType(StrEnum): SUBMODEL = "submodel" def __str__(self) -> str: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model_type_enum.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/another_all_of_sub_model_type_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/another_all_of_sub_model_type_enum.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post.py similarity index 52% rename from end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post.py index 1c255cfcb..df8fdc77f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -1,14 +1,17 @@ +from __future__ import annotations + import datetime import json +from collections.abc import Mapping from io import BytesIO -from typing import TYPE_CHECKING, Any, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field -from dateutil.parser import isoparse +from .. import types from ..models.different_enum import DifferentEnum -from ..types import UNSET, File, FileJsonType, Unset +from ..types import UNSET, File, FileTypes, Unset if TYPE_CHECKING: from ..models.a_form_data import AFormData @@ -34,40 +37,40 @@ class BodyUploadFileTestsUploadPost: some_file (File): some_required_number (float): some_object (BodyUploadFileTestsUploadPostSomeObject): - some_nullable_object (Union['BodyUploadFileTestsUploadPostSomeNullableObject', None]): - some_optional_file (Union[Unset, File]): - some_string (Union[Unset, str]): Default: 'some_default_string'. - a_datetime (Union[Unset, datetime.datetime]): - a_date (Union[Unset, datetime.date]): - some_number (Union[Unset, float]): - some_nullable_number (Union[None, Unset, float]): - some_int_array (Union[Unset, list[Union[None, int]]]): - some_array (Union[None, Unset, list['AFormData']]): - some_optional_object (Union[Unset, BodyUploadFileTestsUploadPostSomeOptionalObject]): - some_enum (Union[Unset, DifferentEnum]): An enumeration. + some_nullable_object (BodyUploadFileTestsUploadPostSomeNullableObject | None): + some_optional_file (File | Unset): + some_string (str | Unset): Default: 'some_default_string'. + a_datetime (datetime.datetime | Unset): + a_date (datetime.date | Unset): + some_number (float | Unset): + some_nullable_number (float | None | Unset): + some_int_array (list[int | None] | Unset): + some_array (list[AFormData] | None | Unset): + some_optional_object (BodyUploadFileTestsUploadPostSomeOptionalObject | Unset): + some_enum (DifferentEnum | Unset): An enumeration. """ some_file: File some_required_number: float - some_object: "BodyUploadFileTestsUploadPostSomeObject" - some_nullable_object: Union["BodyUploadFileTestsUploadPostSomeNullableObject", None] - some_optional_file: Union[Unset, File] = UNSET - some_string: Union[Unset, str] = "some_default_string" - a_datetime: Union[Unset, datetime.datetime] = UNSET - a_date: Union[Unset, datetime.date] = UNSET - some_number: Union[Unset, float] = UNSET - some_nullable_number: Union[None, Unset, float] = UNSET - some_int_array: Union[Unset, list[Union[None, int]]] = UNSET - some_array: Union[None, Unset, list["AFormData"]] = UNSET - some_optional_object: Union[Unset, "BodyUploadFileTestsUploadPostSomeOptionalObject"] = UNSET - some_enum: Union[Unset, DifferentEnum] = UNSET - additional_properties: dict[str, "BodyUploadFileTestsUploadPostAdditionalProperty"] = _attrs_field( + some_object: BodyUploadFileTestsUploadPostSomeObject + some_nullable_object: BodyUploadFileTestsUploadPostSomeNullableObject | None + some_optional_file: File | Unset = UNSET + some_string: str | Unset = "some_default_string" + a_datetime: datetime.datetime | Unset = UNSET + a_date: datetime.date | Unset = UNSET + some_number: float | Unset = UNSET + some_nullable_number: float | None | Unset = UNSET + some_int_array: list[int | None] | Unset = UNSET + some_array: list[AFormData] | None | Unset = UNSET + some_optional_object: BodyUploadFileTestsUploadPostSomeOptionalObject | Unset = UNSET + some_enum: DifferentEnum | Unset = UNSET + additional_properties: dict[str, BodyUploadFileTestsUploadPostAdditionalProperty] = _attrs_field( init=False, factory=dict ) def to_dict(self) -> dict[str, Any]: from ..models.body_upload_file_tests_upload_post_some_nullable_object import ( - BodyUploadFileTestsUploadPostSomeNullableObject, + BodyUploadFileTestsUploadPostSomeNullableObject, # noqa: PLC0415 ) some_file = self.some_file.to_tuple() @@ -76,43 +79,43 @@ def to_dict(self) -> dict[str, Any]: some_object = self.some_object.to_dict() - some_nullable_object: Union[None, dict[str, Any]] + some_nullable_object: dict[str, Any] | None if isinstance(self.some_nullable_object, BodyUploadFileTestsUploadPostSomeNullableObject): some_nullable_object = self.some_nullable_object.to_dict() else: some_nullable_object = self.some_nullable_object - some_optional_file: Union[Unset, FileJsonType] = UNSET + some_optional_file: FileTypes | Unset = UNSET if not isinstance(self.some_optional_file, Unset): some_optional_file = self.some_optional_file.to_tuple() some_string = self.some_string - a_datetime: Union[Unset, str] = UNSET + a_datetime: str | Unset = UNSET if not isinstance(self.a_datetime, Unset): a_datetime = self.a_datetime.isoformat() - a_date: Union[Unset, str] = UNSET + a_date: str | Unset = UNSET if not isinstance(self.a_date, Unset): a_date = self.a_date.isoformat() some_number = self.some_number - some_nullable_number: Union[None, Unset, float] + some_nullable_number: float | None | Unset if isinstance(self.some_nullable_number, Unset): some_nullable_number = UNSET else: some_nullable_number = self.some_nullable_number - some_int_array: Union[Unset, list[Union[None, int]]] = UNSET + some_int_array: list[int | None] | Unset = UNSET if not isinstance(self.some_int_array, Unset): some_int_array = [] for some_int_array_item_data in self.some_int_array: - some_int_array_item: Union[None, int] + some_int_array_item: int | None some_int_array_item = some_int_array_item_data some_int_array.append(some_int_array_item) - some_array: Union[None, Unset, list[dict[str, Any]]] + some_array: list[dict[str, Any]] | None | Unset if isinstance(self.some_array, Unset): some_array = UNSET elif isinstance(self.some_array, list): @@ -124,17 +127,18 @@ def to_dict(self) -> dict[str, Any]: else: some_array = self.some_array - some_optional_object: Union[Unset, dict[str, Any]] = UNSET + some_optional_object: dict[str, Any] | Unset = UNSET if not isinstance(self.some_optional_object, Unset): some_optional_object = self.some_optional_object.to_dict() - some_enum: Union[Unset, str] = UNSET + some_enum: str | Unset = UNSET if not isinstance(self.some_enum, Unset): some_enum = self.some_enum.value field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.to_dict() + field_dict.update( { "some_file": some_file, @@ -166,139 +170,107 @@ def to_dict(self) -> dict[str, Any]: return field_dict - def to_multipart(self) -> dict[str, Any]: - some_file = self.some_file.to_tuple() + def to_multipart(self) -> types.RequestFiles: + from ..models.body_upload_file_tests_upload_post_some_nullable_object import ( + BodyUploadFileTestsUploadPostSomeNullableObject, # noqa: PLC0415 + ) + + files: types.RequestFiles = [] - some_required_number = (None, str(self.some_required_number).encode(), "text/plain") + files.append(("some_file", self.some_file.to_tuple())) - some_object = (None, json.dumps(self.some_object.to_dict()).encode(), "application/json") + files.append(("some_required_number", (None, str(self.some_required_number).encode(), "text/plain"))) - some_nullable_object: tuple[None, bytes, str] + files.append(("some_object", (None, json.dumps(self.some_object.to_dict()).encode(), "application/json"))) if isinstance(self.some_nullable_object, BodyUploadFileTestsUploadPostSomeNullableObject): - some_nullable_object = (None, json.dumps(self.some_nullable_object.to_dict()).encode(), "application/json") + files.append( + ( + "some_nullable_object", + (None, json.dumps(self.some_nullable_object.to_dict()).encode(), "application/json"), + ) + ) else: - some_nullable_object = (None, str(self.some_nullable_object).encode(), "text/plain") + files.append(("some_nullable_object", (None, str(self.some_nullable_object).encode(), "text/plain"))) - some_optional_file: Union[Unset, FileJsonType] = UNSET if not isinstance(self.some_optional_file, Unset): - some_optional_file = self.some_optional_file.to_tuple() + files.append(("some_optional_file", self.some_optional_file.to_tuple())) - some_string = ( - self.some_string - if isinstance(self.some_string, Unset) - else (None, str(self.some_string).encode(), "text/plain") - ) + if not isinstance(self.some_string, Unset): + files.append(("some_string", (None, str(self.some_string).encode(), "text/plain"))) - a_datetime: Union[Unset, bytes] = UNSET if not isinstance(self.a_datetime, Unset): - a_datetime = self.a_datetime.isoformat().encode() + files.append(("a_datetime", (None, self.a_datetime.isoformat().encode(), "text/plain"))) - a_date: Union[Unset, bytes] = UNSET if not isinstance(self.a_date, Unset): - a_date = self.a_date.isoformat().encode() + files.append(("a_date", (None, self.a_date.isoformat().encode(), "text/plain"))) - some_number = ( - self.some_number - if isinstance(self.some_number, Unset) - else (None, str(self.some_number).encode(), "text/plain") - ) + if not isinstance(self.some_number, Unset): + files.append(("some_number", (None, str(self.some_number).encode(), "text/plain"))) - some_nullable_number: Union[Unset, tuple[None, bytes, str]] + if not isinstance(self.some_nullable_number, Unset): + if isinstance(self.some_nullable_number, float): + files.append(("some_nullable_number", (None, str(self.some_nullable_number).encode(), "text/plain"))) + else: + files.append(("some_nullable_number", (None, str(self.some_nullable_number).encode(), "text/plain"))) - if isinstance(self.some_nullable_number, Unset): - some_nullable_number = UNSET - elif isinstance(self.some_nullable_number, float): - some_nullable_number = (None, str(self.some_nullable_number).encode(), "text/plain") - else: - some_nullable_number = (None, str(self.some_nullable_number).encode(), "text/plain") - - some_int_array: Union[Unset, tuple[None, bytes, str]] = UNSET if not isinstance(self.some_int_array, Unset): - _temp_some_int_array = [] - for some_int_array_item_data in self.some_int_array: - some_int_array_item: Union[None, int] - some_int_array_item = some_int_array_item_data - _temp_some_int_array.append(some_int_array_item) - some_int_array = (None, json.dumps(_temp_some_int_array).encode(), "application/json") - - some_array: Union[Unset, tuple[None, bytes, str]] - - if isinstance(self.some_array, Unset): - some_array = UNSET - elif isinstance(self.some_array, list): - _temp_some_array = [] - for some_array_type_0_item_data in self.some_array: - some_array_type_0_item = some_array_type_0_item_data.to_dict() - _temp_some_array.append(some_array_type_0_item) - some_array = (None, json.dumps(_temp_some_array).encode(), "application/json") - else: - some_array = (None, str(self.some_array).encode(), "text/plain") + for some_int_array_item_element in self.some_int_array: + if isinstance(some_int_array_item_element, int): + files.append(("some_int_array", (None, str(some_int_array_item_element).encode(), "text/plain"))) + else: + files.append(("some_int_array", (None, str(some_int_array_item_element).encode(), "text/plain"))) + + if not isinstance(self.some_array, Unset): + if isinstance(self.some_array, list): + for some_array_type_0_item_element in self.some_array: + files.append( + ( + "some_array", + (None, json.dumps(some_array_type_0_item_element.to_dict()).encode(), "application/json"), + ) + ) + else: + files.append(("some_array", (None, str(self.some_array).encode(), "text/plain"))) - some_optional_object: Union[Unset, tuple[None, bytes, str]] = UNSET if not isinstance(self.some_optional_object, Unset): - some_optional_object = (None, json.dumps(self.some_optional_object.to_dict()).encode(), "application/json") + files.append( + ( + "some_optional_object", + (None, json.dumps(self.some_optional_object.to_dict()).encode(), "application/json"), + ) + ) - some_enum: Union[Unset, tuple[None, bytes, str]] = UNSET if not isinstance(self.some_enum, Unset): - some_enum = (None, str(self.some_enum.value).encode(), "text/plain") + files.append(("some_enum", (None, str(self.some_enum.value).encode(), "text/plain"))) - field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): - field_dict[prop_name] = (None, json.dumps(prop.to_dict()).encode(), "application/json") - field_dict.update( - { - "some_file": some_file, - "some_required_number": some_required_number, - "some_object": some_object, - "some_nullable_object": some_nullable_object, - } - ) - if some_optional_file is not UNSET: - field_dict["some_optional_file"] = some_optional_file - if some_string is not UNSET: - field_dict["some_string"] = some_string - if a_datetime is not UNSET: - field_dict["a_datetime"] = a_datetime - if a_date is not UNSET: - field_dict["a_date"] = a_date - if some_number is not UNSET: - field_dict["some_number"] = some_number - if some_nullable_number is not UNSET: - field_dict["some_nullable_number"] = some_nullable_number - if some_int_array is not UNSET: - field_dict["some_int_array"] = some_int_array - if some_array is not UNSET: - field_dict["some_array"] = some_array - if some_optional_object is not UNSET: - field_dict["some_optional_object"] = some_optional_object - if some_enum is not UNSET: - field_dict["some_enum"] = some_enum + files.append((prop_name, (None, json.dumps(prop.to_dict()).encode(), "application/json"))) - return field_dict + return files @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - from ..models.a_form_data import AFormData + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.a_form_data import AFormData # noqa: PLC0415 from ..models.body_upload_file_tests_upload_post_additional_property import ( - BodyUploadFileTestsUploadPostAdditionalProperty, + BodyUploadFileTestsUploadPostAdditionalProperty, # noqa: PLC0415 ) from ..models.body_upload_file_tests_upload_post_some_nullable_object import ( - BodyUploadFileTestsUploadPostSomeNullableObject, + BodyUploadFileTestsUploadPostSomeNullableObject, # noqa: PLC0415 ) - from ..models.body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject + from ..models.body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject # noqa: PLC0415 from ..models.body_upload_file_tests_upload_post_some_optional_object import ( - BodyUploadFileTestsUploadPostSomeOptionalObject, + BodyUploadFileTestsUploadPostSomeOptionalObject, # noqa: PLC0415 ) - d = src_dict.copy() + d = dict(src_dict) some_file = File(payload=BytesIO(d.pop("some_file"))) some_required_number = d.pop("some_required_number") some_object = BodyUploadFileTestsUploadPostSomeObject.from_dict(d.pop("some_object")) - def _parse_some_nullable_object(data: object) -> Union["BodyUploadFileTestsUploadPostSomeNullableObject", None]: + def _parse_some_nullable_object(data: object) -> BodyUploadFileTestsUploadPostSomeNullableObject | None: if data is None: return data try: @@ -307,14 +279,14 @@ def _parse_some_nullable_object(data: object) -> Union["BodyUploadFileTestsUploa some_nullable_object_type_0 = BodyUploadFileTestsUploadPostSomeNullableObject.from_dict(data) return some_nullable_object_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["BodyUploadFileTestsUploadPostSomeNullableObject", None], data) + return cast(BodyUploadFileTestsUploadPostSomeNullableObject | None, data) some_nullable_object = _parse_some_nullable_object(d.pop("some_nullable_object")) _some_optional_file = d.pop("some_optional_file", UNSET) - some_optional_file: Union[Unset, File] + some_optional_file: File | Unset if isinstance(_some_optional_file, Unset): some_optional_file = UNSET else: @@ -323,44 +295,46 @@ def _parse_some_nullable_object(data: object) -> Union["BodyUploadFileTestsUploa some_string = d.pop("some_string", UNSET) _a_datetime = d.pop("a_datetime", UNSET) - a_datetime: Union[Unset, datetime.datetime] + a_datetime: datetime.datetime | Unset if isinstance(_a_datetime, Unset): a_datetime = UNSET else: - a_datetime = isoparse(_a_datetime) + a_datetime = datetime.datetime.fromisoformat(_a_datetime) _a_date = d.pop("a_date", UNSET) - a_date: Union[Unset, datetime.date] + a_date: datetime.date | Unset if isinstance(_a_date, Unset): a_date = UNSET else: - a_date = isoparse(_a_date).date() + a_date = datetime.date.fromisoformat(_a_date) some_number = d.pop("some_number", UNSET) - def _parse_some_nullable_number(data: object) -> Union[None, Unset, float]: + def _parse_some_nullable_number(data: object) -> float | None | Unset: if data is None: return data if isinstance(data, Unset): return data - return cast(Union[None, Unset, float], data) + return cast(float | None | Unset, data) some_nullable_number = _parse_some_nullable_number(d.pop("some_nullable_number", UNSET)) - some_int_array = [] _some_int_array = d.pop("some_int_array", UNSET) - for some_int_array_item_data in _some_int_array or []: + some_int_array: list[int | None] | Unset = UNSET + if _some_int_array is not UNSET: + some_int_array = [] + for some_int_array_item_data in _some_int_array: - def _parse_some_int_array_item(data: object) -> Union[None, int]: - if data is None: - return data - return cast(Union[None, int], data) + def _parse_some_int_array_item(data: object) -> int | None: + if data is None: + return data + return cast(int | None, data) - some_int_array_item = _parse_some_int_array_item(some_int_array_item_data) + some_int_array_item = _parse_some_int_array_item(some_int_array_item_data) - some_int_array.append(some_int_array_item) + some_int_array.append(some_int_array_item) - def _parse_some_array(data: object) -> Union[None, Unset, list["AFormData"]]: + def _parse_some_array(data: object) -> list[AFormData] | None | Unset: if data is None: return data if isinstance(data, Unset): @@ -376,21 +350,21 @@ def _parse_some_array(data: object) -> Union[None, Unset, list["AFormData"]]: some_array_type_0.append(some_array_type_0_item) return some_array_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union[None, Unset, list["AFormData"]], data) + return cast(list[AFormData] | None | Unset, data) some_array = _parse_some_array(d.pop("some_array", UNSET)) _some_optional_object = d.pop("some_optional_object", UNSET) - some_optional_object: Union[Unset, BodyUploadFileTestsUploadPostSomeOptionalObject] + some_optional_object: BodyUploadFileTestsUploadPostSomeOptionalObject | Unset if isinstance(_some_optional_object, Unset): some_optional_object = UNSET else: some_optional_object = BodyUploadFileTestsUploadPostSomeOptionalObject.from_dict(_some_optional_object) _some_enum = d.pop("some_enum", UNSET) - some_enum: Union[Unset, DifferentEnum] + some_enum: DifferentEnum | Unset if isinstance(_some_enum, Unset): some_enum = UNSET else: @@ -426,10 +400,10 @@ def _parse_some_array(data: object) -> Union[None, Unset, list["AFormData"]]: def additional_keys(self) -> list[str]: return list(self.additional_properties.keys()) - def __getitem__(self, key: str) -> "BodyUploadFileTestsUploadPostAdditionalProperty": + def __getitem__(self, key: str) -> BodyUploadFileTestsUploadPostAdditionalProperty: return self.additional_properties[key] - def __setitem__(self, key: str, value: "BodyUploadFileTestsUploadPostAdditionalProperty") -> None: + def __setitem__(self, key: str, value: BodyUploadFileTestsUploadPostAdditionalProperty) -> None: self.additional_properties[key] = value def __delitem__(self, key: str) -> None: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py similarity index 85% rename from end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py index 47dde1338..160cb2542 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,10 +15,10 @@ class BodyUploadFileTestsUploadPostAdditionalProperty: """ Attributes: - foo (Union[Unset, str]): + foo (str | Unset): """ - foo: Union[Unset, str] = UNSET + foo: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -30,8 +33,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) foo = d.pop("foo", UNSET) body_upload_file_tests_upload_post_additional_property = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py similarity index 85% rename from end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py index 817dfd789..47796442d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,10 +15,10 @@ class BodyUploadFileTestsUploadPostSomeNullableObject: """ Attributes: - bar (Union[Unset, str]): + bar (str | Unset): """ - bar: Union[Unset, str] = UNSET + bar: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -30,8 +33,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) bar = d.pop("bar", UNSET) body_upload_file_tests_upload_post_some_nullable_object = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py similarity index 90% rename from end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py index a074bb86f..0cc39b78b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -35,8 +38,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) num = d.pop("num") text = d.pop("text") diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py similarity index 90% rename from end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py index e0ba4b364..f5658196c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -30,8 +33,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) foo = d.pop("foo") body_upload_file_tests_upload_post_some_optional_object = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/different_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/different_enum.py similarity index 67% rename from end_to_end_tests/golden-record/my_test_api_client/models/different_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/different_enum.py index 6c167f25c..3aa4aa4df 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/different_enum.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/different_enum.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class DifferentEnum(str, Enum): +class DifferentEnum(StrEnum): DIFFERENT = "DIFFERENT" OTHER = "OTHER" diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/extended.py similarity index 70% rename from end_to_end_tests/golden-record/my_test_api_client/models/extended.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/extended.py index ffd6406c7..cf4966bf0 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/extended.py @@ -1,10 +1,12 @@ +from __future__ import annotations + import datetime -from typing import TYPE_CHECKING, Any, TypeVar, Union, cast +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast from uuid import UUID from attrs import define as _attrs_define from attrs import field as _attrs_field -from dateutil.parser import isoparse from ..models.an_all_of_enum import AnAllOfEnum from ..models.an_enum import AnEnum @@ -25,65 +27,65 @@ class Extended: Attributes: an_enum_value (AnEnum): For testing Enums in all the ways they can be used an_allof_enum_with_overridden_default (AnAllOfEnum): Default: AnAllOfEnum.OVERRIDDEN_DEFAULT. - a_camel_date_time (Union[datetime.date, datetime.datetime]): + a_camel_date_time (datetime.date | datetime.datetime): a_date (datetime.date): - a_nullable_date (Union[None, datetime.date]): + a_nullable_date (datetime.date | None): a_uuid (UUID): - a_nullable_uuid (Union[None, UUID]): Default: UUID('07EF8B4D-AA09-4FFA-898D-C710796AFF41'). - required_nullable (Union[None, str]): + a_nullable_uuid (None | UUID): Default: UUID('07EF8B4D-AA09-4FFA-898D-C710796AFF41'). + required_nullable (None | str): required_not_nullable (str): - one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Any]): - nullable_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', None]): + one_of_models (Any | FreeFormModel | ModelWithUnionProperty): + nullable_one_of_models (FreeFormModel | ModelWithUnionProperty | None): model (ModelWithUnionProperty): - nullable_model (Union['ModelWithUnionProperty', None]): - any_value (Union[Unset, Any]): Default: 'default'. - an_optional_allof_enum (Union[Unset, AnAllOfEnum]): - nested_list_of_enums (Union[Unset, list[list[DifferentEnum]]]): - a_not_required_date (Union[Unset, datetime.date]): - a_not_required_uuid (Union[Unset, UUID]): - attr_1_leading_digit (Union[Unset, str]): - attr_leading_underscore (Union[Unset, str]): - not_required_nullable (Union[None, Unset, str]): - not_required_not_nullable (Union[Unset, str]): - not_required_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', Unset]): - not_required_nullable_one_of_models (Union['FreeFormModel', 'ModelWithUnionProperty', None, Unset, str]): - not_required_model (Union[Unset, ModelWithUnionProperty]): - not_required_nullable_model (Union['ModelWithUnionProperty', None, Unset]): - from_extended (Union[Unset, str]): + nullable_model (ModelWithUnionProperty | None): + any_value (Any | Unset): Default: 'default'. + an_optional_allof_enum (AnAllOfEnum | Unset): + nested_list_of_enums (list[list[DifferentEnum]] | Unset): + a_not_required_date (datetime.date | Unset): + a_not_required_uuid (UUID | Unset): + attr_1_leading_digit (str | Unset): + attr_leading_underscore (str | Unset): + not_required_nullable (None | str | Unset): + not_required_not_nullable (str | Unset): + not_required_one_of_models (FreeFormModel | ModelWithUnionProperty | Unset): + not_required_nullable_one_of_models (FreeFormModel | ModelWithUnionProperty | None | str | Unset): + not_required_model (ModelWithUnionProperty | Unset): + not_required_nullable_model (ModelWithUnionProperty | None | Unset): + from_extended (str | Unset): """ an_enum_value: AnEnum - a_camel_date_time: Union[datetime.date, datetime.datetime] + a_camel_date_time: datetime.date | datetime.datetime a_date: datetime.date - a_nullable_date: Union[None, datetime.date] + a_nullable_date: datetime.date | None a_uuid: UUID - required_nullable: Union[None, str] + required_nullable: None | str required_not_nullable: str - one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Any] - nullable_one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", None] - model: "ModelWithUnionProperty" - nullable_model: Union["ModelWithUnionProperty", None] + one_of_models: Any | FreeFormModel | ModelWithUnionProperty + nullable_one_of_models: FreeFormModel | ModelWithUnionProperty | None + model: ModelWithUnionProperty + nullable_model: ModelWithUnionProperty | None an_allof_enum_with_overridden_default: AnAllOfEnum = AnAllOfEnum.OVERRIDDEN_DEFAULT - a_nullable_uuid: Union[None, UUID] = UUID("07EF8B4D-AA09-4FFA-898D-C710796AFF41") - any_value: Union[Unset, Any] = "default" - an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET - nested_list_of_enums: Union[Unset, list[list[DifferentEnum]]] = UNSET - a_not_required_date: Union[Unset, datetime.date] = UNSET - a_not_required_uuid: Union[Unset, UUID] = UNSET - attr_1_leading_digit: Union[Unset, str] = UNSET - attr_leading_underscore: Union[Unset, str] = UNSET - not_required_nullable: Union[None, Unset, str] = UNSET - not_required_not_nullable: Union[Unset, str] = UNSET - not_required_one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", Unset] = UNSET - not_required_nullable_one_of_models: Union["FreeFormModel", "ModelWithUnionProperty", None, Unset, str] = UNSET - not_required_model: Union[Unset, "ModelWithUnionProperty"] = UNSET - not_required_nullable_model: Union["ModelWithUnionProperty", None, Unset] = UNSET - from_extended: Union[Unset, str] = UNSET + a_nullable_uuid: None | UUID = UUID("07EF8B4D-AA09-4FFA-898D-C710796AFF41") + any_value: Any | Unset = "default" + an_optional_allof_enum: AnAllOfEnum | Unset = UNSET + nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET + a_not_required_date: datetime.date | Unset = UNSET + a_not_required_uuid: UUID | Unset = UNSET + attr_1_leading_digit: str | Unset = UNSET + attr_leading_underscore: str | Unset = UNSET + not_required_nullable: None | str | Unset = UNSET + not_required_not_nullable: str | Unset = UNSET + not_required_one_of_models: FreeFormModel | ModelWithUnionProperty | Unset = UNSET + not_required_nullable_one_of_models: FreeFormModel | ModelWithUnionProperty | None | str | Unset = UNSET + not_required_model: ModelWithUnionProperty | Unset = UNSET + not_required_nullable_model: ModelWithUnionProperty | None | Unset = UNSET + from_extended: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - from ..models.free_form_model import FreeFormModel - from ..models.model_with_union_property import ModelWithUnionProperty + from ..models.free_form_model import FreeFormModel # noqa: PLC0415 + from ..models.model_with_union_property import ModelWithUnionProperty # noqa: PLC0415 an_enum_value = self.an_enum_value.value @@ -97,7 +99,7 @@ def to_dict(self) -> dict[str, Any]: a_date = self.a_date.isoformat() - a_nullable_date: Union[None, str] + a_nullable_date: None | str if isinstance(self.a_nullable_date, datetime.date): a_nullable_date = self.a_nullable_date.isoformat() else: @@ -105,18 +107,18 @@ def to_dict(self) -> dict[str, Any]: a_uuid = str(self.a_uuid) - a_nullable_uuid: Union[None, str] + a_nullable_uuid: None | str if isinstance(self.a_nullable_uuid, UUID): a_nullable_uuid = str(self.a_nullable_uuid) else: a_nullable_uuid = self.a_nullable_uuid - required_nullable: Union[None, str] + required_nullable: None | str required_nullable = self.required_nullable required_not_nullable = self.required_not_nullable - one_of_models: Union[Any, dict[str, Any]] + one_of_models: Any | dict[str, Any] if isinstance(self.one_of_models, FreeFormModel): one_of_models = self.one_of_models.to_dict() elif isinstance(self.one_of_models, ModelWithUnionProperty): @@ -124,7 +126,7 @@ def to_dict(self) -> dict[str, Any]: else: one_of_models = self.one_of_models - nullable_one_of_models: Union[None, dict[str, Any]] + nullable_one_of_models: dict[str, Any] | None if isinstance(self.nullable_one_of_models, FreeFormModel): nullable_one_of_models = self.nullable_one_of_models.to_dict() elif isinstance(self.nullable_one_of_models, ModelWithUnionProperty): @@ -134,7 +136,7 @@ def to_dict(self) -> dict[str, Any]: model = self.model.to_dict() - nullable_model: Union[None, dict[str, Any]] + nullable_model: dict[str, Any] | None if isinstance(self.nullable_model, ModelWithUnionProperty): nullable_model = self.nullable_model.to_dict() else: @@ -142,11 +144,11 @@ def to_dict(self) -> dict[str, Any]: any_value = self.any_value - an_optional_allof_enum: Union[Unset, str] = UNSET + an_optional_allof_enum: str | Unset = UNSET if not isinstance(self.an_optional_allof_enum, Unset): an_optional_allof_enum = self.an_optional_allof_enum.value - nested_list_of_enums: Union[Unset, list[list[str]]] = UNSET + nested_list_of_enums: list[list[str]] | Unset = UNSET if not isinstance(self.nested_list_of_enums, Unset): nested_list_of_enums = [] for nested_list_of_enums_item_data in self.nested_list_of_enums: @@ -157,11 +159,11 @@ def to_dict(self) -> dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) - a_not_required_date: Union[Unset, str] = UNSET + a_not_required_date: str | Unset = UNSET if not isinstance(self.a_not_required_date, Unset): a_not_required_date = self.a_not_required_date.isoformat() - a_not_required_uuid: Union[Unset, str] = UNSET + a_not_required_uuid: str | Unset = UNSET if not isinstance(self.a_not_required_uuid, Unset): a_not_required_uuid = str(self.a_not_required_uuid) @@ -169,7 +171,7 @@ def to_dict(self) -> dict[str, Any]: attr_leading_underscore = self.attr_leading_underscore - not_required_nullable: Union[None, Unset, str] + not_required_nullable: None | str | Unset if isinstance(self.not_required_nullable, Unset): not_required_nullable = UNSET else: @@ -177,7 +179,7 @@ def to_dict(self) -> dict[str, Any]: not_required_not_nullable = self.not_required_not_nullable - not_required_one_of_models: Union[Unset, dict[str, Any]] + not_required_one_of_models: dict[str, Any] | Unset if isinstance(self.not_required_one_of_models, Unset): not_required_one_of_models = UNSET elif isinstance(self.not_required_one_of_models, FreeFormModel): @@ -185,7 +187,7 @@ def to_dict(self) -> dict[str, Any]: else: not_required_one_of_models = self.not_required_one_of_models.to_dict() - not_required_nullable_one_of_models: Union[None, Unset, dict[str, Any], str] + not_required_nullable_one_of_models: dict[str, Any] | None | str | Unset if isinstance(self.not_required_nullable_one_of_models, Unset): not_required_nullable_one_of_models = UNSET elif isinstance(self.not_required_nullable_one_of_models, FreeFormModel): @@ -195,11 +197,11 @@ def to_dict(self) -> dict[str, Any]: else: not_required_nullable_one_of_models = self.not_required_nullable_one_of_models - not_required_model: Union[Unset, dict[str, Any]] = UNSET + not_required_model: dict[str, Any] | Unset = UNSET if not isinstance(self.not_required_model, Unset): not_required_model = self.not_required_model.to_dict() - not_required_nullable_model: Union[None, Unset, dict[str, Any]] + not_required_nullable_model: dict[str, Any] | None | Unset if isinstance(self.not_required_nullable_model, Unset): not_required_nullable_model = UNSET elif isinstance(self.not_required_nullable_model, ModelWithUnionProperty): @@ -260,52 +262,52 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - from ..models.free_form_model import FreeFormModel - from ..models.model_with_union_property import ModelWithUnionProperty + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.free_form_model import FreeFormModel # noqa: PLC0415 + from ..models.model_with_union_property import ModelWithUnionProperty # noqa: PLC0415 - d = src_dict.copy() + d = dict(src_dict) an_enum_value = AnEnum(d.pop("an_enum_value")) an_allof_enum_with_overridden_default = AnAllOfEnum(d.pop("an_allof_enum_with_overridden_default")) - def _parse_a_camel_date_time(data: object) -> Union[datetime.date, datetime.datetime]: + def _parse_a_camel_date_time(data: object) -> datetime.date | datetime.datetime: try: if not isinstance(data, str): raise TypeError() - a_camel_date_time_type_0 = isoparse(data) + a_camel_date_time_type_0 = datetime.datetime.fromisoformat(data) return a_camel_date_time_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass if not isinstance(data, str): raise TypeError() - a_camel_date_time_type_1 = isoparse(data).date() + a_camel_date_time_type_1 = datetime.date.fromisoformat(data) return a_camel_date_time_type_1 a_camel_date_time = _parse_a_camel_date_time(d.pop("aCamelDateTime")) - a_date = isoparse(d.pop("a_date")).date() + a_date = datetime.date.fromisoformat(d.pop("a_date")) - def _parse_a_nullable_date(data: object) -> Union[None, datetime.date]: + def _parse_a_nullable_date(data: object) -> datetime.date | None: if data is None: return data try: if not isinstance(data, str): raise TypeError() - a_nullable_date_type_0 = isoparse(data).date() + a_nullable_date_type_0 = datetime.date.fromisoformat(data) return a_nullable_date_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union[None, datetime.date], data) + return cast(datetime.date | None, data) a_nullable_date = _parse_a_nullable_date(d.pop("a_nullable_date")) a_uuid = UUID(d.pop("a_uuid")) - def _parse_a_nullable_uuid(data: object) -> Union[None, UUID]: + def _parse_a_nullable_uuid(data: object) -> None | UUID: if data is None: return data try: @@ -314,29 +316,29 @@ def _parse_a_nullable_uuid(data: object) -> Union[None, UUID]: a_nullable_uuid_type_0 = UUID(data) return a_nullable_uuid_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union[None, UUID], data) + return cast(None | UUID, data) a_nullable_uuid = _parse_a_nullable_uuid(d.pop("a_nullable_uuid")) - def _parse_required_nullable(data: object) -> Union[None, str]: + def _parse_required_nullable(data: object) -> None | str: if data is None: return data - return cast(Union[None, str], data) + return cast(None | str, data) required_nullable = _parse_required_nullable(d.pop("required_nullable")) required_not_nullable = d.pop("required_not_nullable") - def _parse_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", Any]: + def _parse_one_of_models(data: object) -> Any | FreeFormModel | ModelWithUnionProperty: try: if not isinstance(data, dict): raise TypeError() one_of_models_type_0 = FreeFormModel.from_dict(data) return one_of_models_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass try: if not isinstance(data, dict): @@ -344,13 +346,13 @@ def _parse_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnion one_of_models_type_1 = ModelWithUnionProperty.from_dict(data) return one_of_models_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["FreeFormModel", "ModelWithUnionProperty", Any], data) + return cast(Any | FreeFormModel | ModelWithUnionProperty, data) one_of_models = _parse_one_of_models(d.pop("one_of_models")) - def _parse_nullable_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", None]: + def _parse_nullable_one_of_models(data: object) -> FreeFormModel | ModelWithUnionProperty | None: if data is None: return data try: @@ -359,7 +361,7 @@ def _parse_nullable_one_of_models(data: object) -> Union["FreeFormModel", "Model nullable_one_of_models_type_0 = FreeFormModel.from_dict(data) return nullable_one_of_models_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass try: if not isinstance(data, dict): @@ -367,15 +369,15 @@ def _parse_nullable_one_of_models(data: object) -> Union["FreeFormModel", "Model nullable_one_of_models_type_1 = ModelWithUnionProperty.from_dict(data) return nullable_one_of_models_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["FreeFormModel", "ModelWithUnionProperty", None], data) + return cast(FreeFormModel | ModelWithUnionProperty | None, data) nullable_one_of_models = _parse_nullable_one_of_models(d.pop("nullable_one_of_models")) model = ModelWithUnionProperty.from_dict(d.pop("model")) - def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None]: + def _parse_nullable_model(data: object) -> ModelWithUnionProperty | None: if data is None: return data try: @@ -384,42 +386,44 @@ def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None] nullable_model_type_1 = ModelWithUnionProperty.from_dict(data) return nullable_model_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["ModelWithUnionProperty", None], data) + return cast(ModelWithUnionProperty | None, data) nullable_model = _parse_nullable_model(d.pop("nullable_model")) any_value = d.pop("any_value", UNSET) _an_optional_allof_enum = d.pop("an_optional_allof_enum", UNSET) - an_optional_allof_enum: Union[Unset, AnAllOfEnum] + an_optional_allof_enum: AnAllOfEnum | Unset if isinstance(_an_optional_allof_enum, Unset): an_optional_allof_enum = UNSET else: an_optional_allof_enum = AnAllOfEnum(_an_optional_allof_enum) - nested_list_of_enums = [] _nested_list_of_enums = d.pop("nested_list_of_enums", UNSET) - for nested_list_of_enums_item_data in _nested_list_of_enums or []: - nested_list_of_enums_item = [] - _nested_list_of_enums_item = nested_list_of_enums_item_data - for nested_list_of_enums_item_item_data in _nested_list_of_enums_item: - nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) + nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET + if _nested_list_of_enums is not UNSET: + nested_list_of_enums = [] + for nested_list_of_enums_item_data in _nested_list_of_enums: + nested_list_of_enums_item = [] + _nested_list_of_enums_item = nested_list_of_enums_item_data + for nested_list_of_enums_item_item_data in _nested_list_of_enums_item: + nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) - nested_list_of_enums_item.append(nested_list_of_enums_item_item) + nested_list_of_enums_item.append(nested_list_of_enums_item_item) - nested_list_of_enums.append(nested_list_of_enums_item) + nested_list_of_enums.append(nested_list_of_enums_item) _a_not_required_date = d.pop("a_not_required_date", UNSET) - a_not_required_date: Union[Unset, datetime.date] + a_not_required_date: datetime.date | Unset if isinstance(_a_not_required_date, Unset): a_not_required_date = UNSET else: - a_not_required_date = isoparse(_a_not_required_date).date() + a_not_required_date = datetime.date.fromisoformat(_a_not_required_date) _a_not_required_uuid = d.pop("a_not_required_uuid", UNSET) - a_not_required_uuid: Union[Unset, UUID] + a_not_required_uuid: UUID | Unset if isinstance(_a_not_required_uuid, Unset): a_not_required_uuid = UNSET else: @@ -429,18 +433,18 @@ def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None] attr_leading_underscore = d.pop("_leading_underscore", UNSET) - def _parse_not_required_nullable(data: object) -> Union[None, Unset, str]: + def _parse_not_required_nullable(data: object) -> None | str | Unset: if data is None: return data if isinstance(data, Unset): return data - return cast(Union[None, Unset, str], data) + return cast(None | str | Unset, data) not_required_nullable = _parse_not_required_nullable(d.pop("not_required_nullable", UNSET)) not_required_not_nullable = d.pop("not_required_not_nullable", UNSET) - def _parse_not_required_one_of_models(data: object) -> Union["FreeFormModel", "ModelWithUnionProperty", Unset]: + def _parse_not_required_one_of_models(data: object) -> FreeFormModel | ModelWithUnionProperty | Unset: if isinstance(data, Unset): return data try: @@ -449,7 +453,7 @@ def _parse_not_required_one_of_models(data: object) -> Union["FreeFormModel", "M not_required_one_of_models_type_0 = FreeFormModel.from_dict(data) return not_required_one_of_models_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass if not isinstance(data, dict): raise TypeError() @@ -461,7 +465,7 @@ def _parse_not_required_one_of_models(data: object) -> Union["FreeFormModel", "M def _parse_not_required_nullable_one_of_models( data: object, - ) -> Union["FreeFormModel", "ModelWithUnionProperty", None, Unset, str]: + ) -> FreeFormModel | ModelWithUnionProperty | None | str | Unset: if data is None: return data if isinstance(data, Unset): @@ -472,7 +476,7 @@ def _parse_not_required_nullable_one_of_models( not_required_nullable_one_of_models_type_0 = FreeFormModel.from_dict(data) return not_required_nullable_one_of_models_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass try: if not isinstance(data, dict): @@ -480,22 +484,22 @@ def _parse_not_required_nullable_one_of_models( not_required_nullable_one_of_models_type_1 = ModelWithUnionProperty.from_dict(data) return not_required_nullable_one_of_models_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["FreeFormModel", "ModelWithUnionProperty", None, Unset, str], data) + return cast(FreeFormModel | ModelWithUnionProperty | None | str | Unset, data) not_required_nullable_one_of_models = _parse_not_required_nullable_one_of_models( d.pop("not_required_nullable_one_of_models", UNSET) ) _not_required_model = d.pop("not_required_model", UNSET) - not_required_model: Union[Unset, ModelWithUnionProperty] + not_required_model: ModelWithUnionProperty | Unset if isinstance(_not_required_model, Unset): not_required_model = UNSET else: not_required_model = ModelWithUnionProperty.from_dict(_not_required_model) - def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionProperty", None, Unset]: + def _parse_not_required_nullable_model(data: object) -> ModelWithUnionProperty | None | Unset: if data is None: return data if isinstance(data, Unset): @@ -506,9 +510,9 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro not_required_nullable_model_type_1 = ModelWithUnionProperty.from_dict(data) return not_required_nullable_model_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["ModelWithUnionProperty", None, Unset], data) + return cast(ModelWithUnionProperty | None | Unset, data) not_required_nullable_model = _parse_not_required_nullable_model(d.pop("not_required_nullable_model", UNSET)) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/free_form_model.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/free_form_model.py similarity index 86% rename from end_to_end_tests/golden-record/my_test_api_client/models/free_form_model.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/free_form_model.py index 10770503b..b8465ef44 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/free_form_model.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/free_form_model.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -8,19 +11,18 @@ @_attrs_define class FreeFormModel: - """ """ - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) free_form_model = cls() free_form_model.additional_properties = d diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/get_location_header_types_int_enum_header.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_location_header_types_int_enum_header.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/models/get_location_header_types_int_enum_header.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_location_header_types_int_enum_header.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/get_location_header_types_string_enum_header.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_location_header_types_string_enum_header.py similarity index 59% rename from end_to_end_tests/golden-record/my_test_api_client/models/get_location_header_types_string_enum_header.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_location_header_types_string_enum_header.py index cce92dcde..2a5242fe5 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/get_location_header_types_string_enum_header.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_location_header_types_string_enum_header.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class GetLocationHeaderTypesStringEnumHeader(str, Enum): +class GetLocationHeaderTypesStringEnumHeader(StrEnum): ONE = "one" THREE = "three" TWO = "two" diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/get_models_allof_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_allof_response_200.py similarity index 72% rename from end_to_end_tests/golden-record/my_test_api_client/models/get_models_allof_response_200.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_allof_response_200.py index 5e24734e3..9ec5f68c8 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/get_models_allof_response_200.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_allof_response_200.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -17,26 +20,26 @@ class GetModelsAllofResponse200: """ Attributes: - aliased (Union[Unset, AModel]): A Model for testing all the ways custom objects can be used - extended (Union[Unset, Extended]): - model (Union[Unset, AModel]): A Model for testing all the ways custom objects can be used + aliased (AModel | Unset): A Model for testing all the ways custom objects can be used + extended (Extended | Unset): + model (AModel | Unset): A Model for testing all the ways custom objects can be used """ - aliased: Union[Unset, "AModel"] = UNSET - extended: Union[Unset, "Extended"] = UNSET - model: Union[Unset, "AModel"] = UNSET + aliased: AModel | Unset = UNSET + extended: Extended | Unset = UNSET + model: AModel | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - aliased: Union[Unset, dict[str, Any]] = UNSET + aliased: dict[str, Any] | Unset = UNSET if not isinstance(self.aliased, Unset): aliased = self.aliased.to_dict() - extended: Union[Unset, dict[str, Any]] = UNSET + extended: dict[str, Any] | Unset = UNSET if not isinstance(self.extended, Unset): extended = self.extended.to_dict() - model: Union[Unset, dict[str, Any]] = UNSET + model: dict[str, Any] | Unset = UNSET if not isinstance(self.model, Unset): model = self.model.to_dict() @@ -53,27 +56,27 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - from ..models.a_model import AModel - from ..models.extended import Extended + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.a_model import AModel # noqa: PLC0415 + from ..models.extended import Extended # noqa: PLC0415 - d = src_dict.copy() + d = dict(src_dict) _aliased = d.pop("aliased", UNSET) - aliased: Union[Unset, AModel] + aliased: AModel | Unset if isinstance(_aliased, Unset): aliased = UNSET else: aliased = AModel.from_dict(_aliased) _extended = d.pop("extended", UNSET) - extended: Union[Unset, Extended] + extended: Extended | Unset if isinstance(_extended, Unset): extended = UNSET else: extended = Extended.from_dict(_extended) _model = d.pop("model", UNSET) - model: Union[Unset, AModel] + model: AModel | Unset if isinstance(_model, Unset): model = UNSET else: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_0.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_0.py similarity index 87% rename from end_to_end_tests/golden-record/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_0.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_0.py index 18c1e9e64..a079b954a 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_0.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_0.py @@ -1,4 +1,7 @@ -from typing import Any, Literal, TypeVar, Union, cast +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Literal, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -13,11 +16,11 @@ class GetModelsOneofWithRequiredConstResponse200Type0: """ Attributes: type_ (Literal['alpha']): - color (Union[Unset, str]): + color (str | Unset): """ type_: Literal["alpha"] - color: Union[Unset, str] = UNSET + color: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -38,8 +41,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) type_ = cast(Literal["alpha"], d.pop("type")) if type_ != "alpha": raise ValueError(f"type must match const 'alpha', got '{type_}'") diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_1.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_1.py similarity index 87% rename from end_to_end_tests/golden-record/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_1.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_1.py index b1df54c32..04670dcba 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_1.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/get_models_oneof_with_required_const_response_200_type_1.py @@ -1,4 +1,7 @@ -from typing import Any, Literal, TypeVar, Union, cast +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Literal, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -13,11 +16,11 @@ class GetModelsOneofWithRequiredConstResponse200Type1: """ Attributes: type_ (Literal['beta']): - texture (Union[Unset, str]): + texture (str | Unset): """ type_: Literal["beta"] - texture: Union[Unset, str] = UNSET + texture: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -38,8 +41,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) type_ = cast(Literal["beta"], d.pop("type")) if type_ != "beta": raise ValueError(f"type must match const 'beta', got '{type_}'") diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/http_validation_error.py similarity index 55% rename from end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/http_validation_error.py index a423c6d2f..7ed970dde 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/http_validation_error.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define @@ -15,13 +18,13 @@ class HTTPValidationError: """ Attributes: - detail (Union[Unset, list['ValidationError']]): + detail (list[ValidationError] | Unset): """ - detail: Union[Unset, list["ValidationError"]] = UNSET + detail: list[ValidationError] | Unset = UNSET def to_dict(self) -> dict[str, Any]: - detail: Union[Unset, list[dict[str, Any]]] = UNSET + detail: list[dict[str, Any]] | Unset = UNSET if not isinstance(self.detail, Unset): detail = [] for detail_item_data in self.detail: @@ -29,6 +32,7 @@ def to_dict(self) -> dict[str, Any]: detail.append(detail_item) field_dict: dict[str, Any] = {} + field_dict.update({}) if detail is not UNSET: field_dict["detail"] = detail @@ -36,16 +40,18 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - from ..models.validation_error import ValidationError + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.validation_error import ValidationError # noqa: PLC0415 - d = src_dict.copy() - detail = [] + d = dict(src_dict) _detail = d.pop("detail", UNSET) - for detail_item_data in _detail or []: - detail_item = ValidationError.from_dict(detail_item_data) + detail: list[ValidationError] | Unset = UNSET + if _detail is not UNSET: + detail = [] + for detail_item_data in _detail: + detail_item = ValidationError.from_dict(detail_item_data) - detail.append(detail_item) + detail.append(detail_item) http_validation_error = cls( detail=detail, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/import_.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/import_.py similarity index 86% rename from end_to_end_tests/golden-record/my_test_api_client/models/import_.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/import_.py index 79788bf80..a24ec3f3d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/import_.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/import_.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -8,19 +11,18 @@ @_attrs_define class Import: - """ """ - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) import_ = cls() import_.additional_properties = d diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/json_like_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/json_like_body.py similarity index 83% rename from end_to_end_tests/golden-record/my_test_api_client/models/json_like_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/json_like_body.py index bb4a31010..4e60f7ab6 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/json_like_body.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/json_like_body.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,10 +15,10 @@ class JsonLikeBody: """ Attributes: - a (Union[Unset, str]): + a (str | Unset): """ - a: Union[Unset, str] = UNSET + a: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -30,8 +33,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) a = d.pop("a", UNSET) json_like_body = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/mixed_case_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/mixed_case_response_200.py similarity index 82% rename from end_to_end_tests/golden-record/my_test_api_client/models/mixed_case_response_200.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/mixed_case_response_200.py index adb74459d..93aead870 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/mixed_case_response_200.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/mixed_case_response_200.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,12 +15,12 @@ class MixedCaseResponse200: """ Attributes: - mixed_case (Union[Unset, str]): - mixedCase (Union[Unset, str]): + mixed_case (str | Unset): + mixedCase (str | Unset): """ - mixed_case: Union[Unset, str] = UNSET - mixedCase: Union[Unset, str] = UNSET + mixed_case: str | Unset = UNSET + mixedCase: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -36,8 +39,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) mixed_case = d.pop("mixed_case", UNSET) mixedCase = d.pop("mixedCase", UNSET) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_from_all_of.py similarity index 77% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_from_all_of.py index a9fb59976..a8abde58a 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_from_all_of.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_from_all_of.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -14,26 +17,26 @@ class ModelFromAllOf: """ Attributes: - a_sub_property (Union[Unset, str]): - type_ (Union[Unset, AnotherAllOfSubModelType]): - type_enum (Union[Unset, AnotherAllOfSubModelTypeEnum]): - another_sub_property (Union[Unset, str]): + a_sub_property (str | Unset): + type_ (AnotherAllOfSubModelType | Unset): + type_enum (AnotherAllOfSubModelTypeEnum | Unset): + another_sub_property (str | Unset): """ - a_sub_property: Union[Unset, str] = UNSET - type_: Union[Unset, AnotherAllOfSubModelType] = UNSET - type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] = UNSET - another_sub_property: Union[Unset, str] = UNSET + a_sub_property: str | Unset = UNSET + type_: AnotherAllOfSubModelType | Unset = UNSET + type_enum: AnotherAllOfSubModelTypeEnum | Unset = UNSET + another_sub_property: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: a_sub_property = self.a_sub_property - type_: Union[Unset, str] = UNSET + type_: str | Unset = UNSET if not isinstance(self.type_, Unset): type_ = self.type_.value - type_enum: Union[Unset, int] = UNSET + type_enum: int | Unset = UNSET if not isinstance(self.type_enum, Unset): type_enum = self.type_enum.value @@ -54,19 +57,19 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) a_sub_property = d.pop("a_sub_property", UNSET) _type_ = d.pop("type", UNSET) - type_: Union[Unset, AnotherAllOfSubModelType] + type_: AnotherAllOfSubModelType | Unset if isinstance(_type_, Unset): type_ = UNSET else: type_ = AnotherAllOfSubModelType(_type_) _type_enum = d.pop("type_enum", UNSET) - type_enum: Union[Unset, AnotherAllOfSubModelTypeEnum] + type_enum: AnotherAllOfSubModelTypeEnum | Unset if isinstance(_type_enum, Unset): type_enum = UNSET else: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_name.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_name.py similarity index 86% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_name.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_name.py index 6f4eefc36..24703fa14 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_name.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_name.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -8,19 +11,18 @@ @_attrs_define class ModelName: - """ """ - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) model_name = cls() model_name.additional_properties = d diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_reference_with_periods.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_reference_with_periods.py similarity index 87% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_reference_with_periods.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_reference_with_periods.py index 004ed9b20..52caf65bb 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_reference_with_periods.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_reference_with_periods.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -13,14 +16,15 @@ class ModelReferenceWithPeriods: additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) model_reference_with_periods = cls() model_reference_with_periods.additional_properties = d diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_inlined.py similarity index 76% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_inlined.py index a0cdb4f6a..c1ca5e44c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_inlined.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -18,11 +21,11 @@ class ModelWithAdditionalPropertiesInlined: """ Attributes: - a_number (Union[Unset, float]): + a_number (float | Unset): """ - a_number: Union[Unset, float] = UNSET - additional_properties: dict[str, "ModelWithAdditionalPropertiesInlinedAdditionalProperty"] = _attrs_field( + a_number: float | Unset = UNSET + additional_properties: dict[str, ModelWithAdditionalPropertiesInlinedAdditionalProperty] = _attrs_field( init=False, factory=dict ) @@ -32,6 +35,7 @@ def to_dict(self) -> dict[str, Any]: field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.to_dict() + field_dict.update({}) if a_number is not UNSET: field_dict["a_number"] = a_number @@ -39,12 +43,12 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.model_with_additional_properties_inlined_additional_property import ( - ModelWithAdditionalPropertiesInlinedAdditionalProperty, + ModelWithAdditionalPropertiesInlinedAdditionalProperty, # noqa: PLC0415 ) - d = src_dict.copy() + d = dict(src_dict) a_number = d.pop("a_number", UNSET) model_with_additional_properties_inlined = cls( @@ -64,10 +68,10 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def additional_keys(self) -> list[str]: return list(self.additional_properties.keys()) - def __getitem__(self, key: str) -> "ModelWithAdditionalPropertiesInlinedAdditionalProperty": + def __getitem__(self, key: str) -> ModelWithAdditionalPropertiesInlinedAdditionalProperty: return self.additional_properties[key] - def __setitem__(self, key: str, value: "ModelWithAdditionalPropertiesInlinedAdditionalProperty") -> None: + def __setitem__(self, key: str, value: ModelWithAdditionalPropertiesInlinedAdditionalProperty) -> None: self.additional_properties[key] = value def __delitem__(self, key: str) -> None: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py similarity index 85% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py index 3bfffaf2c..8f877a575 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_inlined_additional_property.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,10 +15,10 @@ class ModelWithAdditionalPropertiesInlinedAdditionalProperty: """ Attributes: - extra_props_prop (Union[Unset, str]): + extra_props_prop (str | Unset): """ - extra_props_prop: Union[Unset, str] = UNSET + extra_props_prop: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -30,8 +33,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) extra_props_prop = d.pop("extra_props_prop", UNSET) model_with_additional_properties_inlined_additional_property = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_refed.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_refed.py similarity index 90% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_refed.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_refed.py index 0b80a0076..a7ef955fd 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_additional_properties_refed.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_additional_properties_refed.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -10,11 +13,10 @@ @_attrs_define class ModelWithAdditionalPropertiesRefed: - """ """ - additional_properties: dict[str, AnEnum] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.value @@ -22,8 +24,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) model_with_additional_properties_refed = cls() additional_properties = {} diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties.py index f71fe7c1e..f3d23571c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union, cast +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -14,15 +17,13 @@ @_attrs_define class ModelWithAnyJsonProperties: - """ """ - additional_properties: dict[ - str, Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", bool, float, int, list[str], str] + str, bool | float | int | list[str] | ModelWithAnyJsonPropertiesAdditionalPropertyType0 | str ] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.model_with_any_json_properties_additional_property_type_0 import ( - ModelWithAnyJsonPropertiesAdditionalPropertyType0, + ModelWithAnyJsonPropertiesAdditionalPropertyType0, # noqa: PLC0415 ) field_dict: dict[str, Any] = {} @@ -38,12 +39,12 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.model_with_any_json_properties_additional_property_type_0 import ( - ModelWithAnyJsonPropertiesAdditionalPropertyType0, + ModelWithAnyJsonPropertiesAdditionalPropertyType0, # noqa: PLC0415 ) - d = src_dict.copy() + d = dict(src_dict) model_with_any_json_properties = cls() additional_properties = {} @@ -51,14 +52,14 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def _parse_additional_property( data: object, - ) -> Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", bool, float, int, list[str], str]: + ) -> bool | float | int | list[str] | ModelWithAnyJsonPropertiesAdditionalPropertyType0 | str: try: if not isinstance(data, dict): raise TypeError() additional_property_type_0 = ModelWithAnyJsonPropertiesAdditionalPropertyType0.from_dict(data) return additional_property_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass try: if not isinstance(data, list): @@ -66,10 +67,10 @@ def _parse_additional_property( additional_property_type_1 = cast(list[str], data) return additional_property_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass return cast( - Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", bool, float, int, list[str], str], data + bool | float | int | list[str] | ModelWithAnyJsonPropertiesAdditionalPropertyType0 | str, data ) additional_property = _parse_additional_property(prop_dict) @@ -85,13 +86,11 @@ def additional_keys(self) -> list[str]: def __getitem__( self, key: str - ) -> Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", bool, float, int, list[str], str]: + ) -> bool | float | int | list[str] | ModelWithAnyJsonPropertiesAdditionalPropertyType0 | str: return self.additional_properties[key] def __setitem__( - self, - key: str, - value: Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", bool, float, int, list[str], str], + self, key: str, value: bool | float | int | list[str] | ModelWithAnyJsonPropertiesAdditionalPropertyType0 | str ) -> None: self.additional_properties[key] = value diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py similarity index 88% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py index 65993e8be..9d75dc52c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -8,19 +11,18 @@ @_attrs_define class ModelWithAnyJsonPropertiesAdditionalPropertyType0: - """ """ - additional_properties: dict[str, str] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) model_with_any_json_properties_additional_property_type_0 = cls() model_with_any_json_properties_additional_property_type_0.additional_properties = d diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_backslash_in_description.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_backslash_in_description.py similarity index 84% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_backslash_in_description.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_backslash_in_description.py index 0b5dfc3b5..3492b738f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_backslash_in_description.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_backslash_in_description.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -8,21 +11,22 @@ @_attrs_define class ModelWithBackslashInDescription: - r""" Description with special character: \ + """ Description with special character: \\ """ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) model_with_backslash_in_description = cls() model_with_backslash_in_description.additional_properties = d diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_a.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_a.py similarity index 79% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_a.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_a.py index 3253c520b..a9bde2daf 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_a.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_a.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -16,14 +19,14 @@ class ModelWithCircularRefA: """ Attributes: - circular (Union[Unset, ModelWithCircularRefB]): + circular (ModelWithCircularRefB | Unset): """ - circular: Union[Unset, "ModelWithCircularRefB"] = UNSET + circular: ModelWithCircularRefB | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - circular: Union[Unset, dict[str, Any]] = UNSET + circular: dict[str, Any] | Unset = UNSET if not isinstance(self.circular, Unset): circular = self.circular.to_dict() @@ -36,12 +39,12 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - from ..models.model_with_circular_ref_b import ModelWithCircularRefB + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.model_with_circular_ref_b import ModelWithCircularRefB # noqa: PLC0415 - d = src_dict.copy() + d = dict(src_dict) _circular = d.pop("circular", UNSET) - circular: Union[Unset, ModelWithCircularRefB] + circular: ModelWithCircularRefB | Unset if isinstance(_circular, Unset): circular = UNSET else: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_b.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_b.py similarity index 79% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_b.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_b.py index 89c3a064c..c21d33148 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_b.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_b.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -16,14 +19,14 @@ class ModelWithCircularRefB: """ Attributes: - circular (Union[Unset, ModelWithCircularRefA]): + circular (ModelWithCircularRefA | Unset): """ - circular: Union[Unset, "ModelWithCircularRefA"] = UNSET + circular: ModelWithCircularRefA | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - circular: Union[Unset, dict[str, Any]] = UNSET + circular: dict[str, Any] | Unset = UNSET if not isinstance(self.circular, Unset): circular = self.circular.to_dict() @@ -36,12 +39,12 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - from ..models.model_with_circular_ref_a import ModelWithCircularRefA + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.model_with_circular_ref_a import ModelWithCircularRefA # noqa: PLC0415 - d = src_dict.copy() + d = dict(src_dict) _circular = d.pop("circular", UNSET) - circular: Union[Unset, ModelWithCircularRefA] + circular: ModelWithCircularRefA | Unset if isinstance(_circular, Unset): circular = UNSET else: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py similarity index 76% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py index 32cb687c7..fdc790779 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_a.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define @@ -12,13 +15,12 @@ @_attrs_define class ModelWithCircularRefInAdditionalPropertiesA: - """ """ - - additional_properties: dict[str, "ModelWithCircularRefInAdditionalPropertiesB"] = _attrs_field( + additional_properties: dict[str, ModelWithCircularRefInAdditionalPropertiesB] = _attrs_field( init=False, factory=dict ) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.to_dict() @@ -26,12 +28,12 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.model_with_circular_ref_in_additional_properties_b import ( - ModelWithCircularRefInAdditionalPropertiesB, + ModelWithCircularRefInAdditionalPropertiesB, # noqa: PLC0415 ) - d = src_dict.copy() + d = dict(src_dict) model_with_circular_ref_in_additional_properties_a = cls() additional_properties = {} @@ -47,10 +49,10 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def additional_keys(self) -> list[str]: return list(self.additional_properties.keys()) - def __getitem__(self, key: str) -> "ModelWithCircularRefInAdditionalPropertiesB": + def __getitem__(self, key: str) -> ModelWithCircularRefInAdditionalPropertiesB: return self.additional_properties[key] - def __setitem__(self, key: str, value: "ModelWithCircularRefInAdditionalPropertiesB") -> None: + def __setitem__(self, key: str, value: ModelWithCircularRefInAdditionalPropertiesB) -> None: self.additional_properties[key] = value def __delitem__(self, key: str) -> None: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py similarity index 76% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py index d134a94b9..1c3f1998c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_circular_ref_in_additional_properties_b.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define @@ -12,13 +15,12 @@ @_attrs_define class ModelWithCircularRefInAdditionalPropertiesB: - """ """ - - additional_properties: dict[str, "ModelWithCircularRefInAdditionalPropertiesA"] = _attrs_field( + additional_properties: dict[str, ModelWithCircularRefInAdditionalPropertiesA] = _attrs_field( init=False, factory=dict ) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.to_dict() @@ -26,12 +28,12 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.model_with_circular_ref_in_additional_properties_a import ( - ModelWithCircularRefInAdditionalPropertiesA, + ModelWithCircularRefInAdditionalPropertiesA, # noqa: PLC0415 ) - d = src_dict.copy() + d = dict(src_dict) model_with_circular_ref_in_additional_properties_b = cls() additional_properties = {} @@ -47,10 +49,10 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def additional_keys(self) -> list[str]: return list(self.additional_properties.keys()) - def __getitem__(self, key: str) -> "ModelWithCircularRefInAdditionalPropertiesA": + def __getitem__(self, key: str) -> ModelWithCircularRefInAdditionalPropertiesA: return self.additional_properties[key] - def __setitem__(self, key: str, value: "ModelWithCircularRefInAdditionalPropertiesA") -> None: + def __setitem__(self, key: str, value: ModelWithCircularRefInAdditionalPropertiesA) -> None: self.additional_properties[key] = value def __delitem__(self, key: str) -> None: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_date_time_property.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_date_time_property.py similarity index 77% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_date_time_property.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_date_time_property.py index f503af00a..b005efa02 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_date_time_property.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_date_time_property.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import datetime -from typing import Any, TypeVar, Union +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field -from dateutil.parser import isoparse from ..types import UNSET, Unset @@ -14,14 +16,14 @@ class ModelWithDateTimeProperty: """ Attributes: - datetime_ (Union[Unset, datetime.datetime]): + datetime_ (datetime.datetime | Unset): """ - datetime_: Union[Unset, datetime.datetime] = UNSET + datetime_: datetime.datetime | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - datetime_: Union[Unset, str] = UNSET + datetime_: str | Unset = UNSET if not isinstance(self.datetime_, Unset): datetime_ = self.datetime_.isoformat() @@ -34,14 +36,14 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) _datetime_ = d.pop("datetime", UNSET) - datetime_: Union[Unset, datetime.datetime] + datetime_: datetime.datetime | Unset if isinstance(_datetime_, Unset): datetime_ = UNSET else: - datetime_ = isoparse(_datetime_) + datetime_ = datetime.datetime.fromisoformat(_datetime_) model_with_date_time_property = cls( datetime_=datetime_, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_discriminated_union.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_discriminated_union.py similarity index 77% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_discriminated_union.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_discriminated_union.py index 93a3535db..41b972dc4 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_discriminated_union.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_discriminated_union.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union, cast +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -17,17 +20,17 @@ class ModelWithDiscriminatedUnion: """ Attributes: - discriminated_union (Union['ADiscriminatedUnionType1', 'ADiscriminatedUnionType2', None, Unset]): + discriminated_union (ADiscriminatedUnionType1 | ADiscriminatedUnionType2 | None | Unset): """ - discriminated_union: Union["ADiscriminatedUnionType1", "ADiscriminatedUnionType2", None, Unset] = UNSET + discriminated_union: ADiscriminatedUnionType1 | ADiscriminatedUnionType2 | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - from ..models.a_discriminated_union_type_1 import ADiscriminatedUnionType1 - from ..models.a_discriminated_union_type_2 import ADiscriminatedUnionType2 + from ..models.a_discriminated_union_type_1 import ADiscriminatedUnionType1 # noqa: PLC0415 + from ..models.a_discriminated_union_type_2 import ADiscriminatedUnionType2 # noqa: PLC0415 - discriminated_union: Union[None, Unset, dict[str, Any]] + discriminated_union: dict[str, Any] | None | Unset if isinstance(self.discriminated_union, Unset): discriminated_union = UNSET elif isinstance(self.discriminated_union, ADiscriminatedUnionType1): @@ -46,15 +49,15 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - from ..models.a_discriminated_union_type_1 import ADiscriminatedUnionType1 - from ..models.a_discriminated_union_type_2 import ADiscriminatedUnionType2 + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.a_discriminated_union_type_1 import ADiscriminatedUnionType1 # noqa: PLC0415 + from ..models.a_discriminated_union_type_2 import ADiscriminatedUnionType2 # noqa: PLC0415 - d = src_dict.copy() + d = dict(src_dict) def _parse_discriminated_union( data: object, - ) -> Union["ADiscriminatedUnionType1", "ADiscriminatedUnionType2", None, Unset]: + ) -> ADiscriminatedUnionType1 | ADiscriminatedUnionType2 | None | Unset: if data is None: return data if isinstance(data, Unset): @@ -65,7 +68,7 @@ def _parse_discriminated_union( componentsschemas_a_discriminated_union_type_0 = ADiscriminatedUnionType1.from_dict(data) return componentsschemas_a_discriminated_union_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass try: if not isinstance(data, dict): @@ -73,9 +76,9 @@ def _parse_discriminated_union( componentsschemas_a_discriminated_union_type_1 = ADiscriminatedUnionType2.from_dict(data) return componentsschemas_a_discriminated_union_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["ADiscriminatedUnionType1", "ADiscriminatedUnionType2", None, Unset], data) + return cast(ADiscriminatedUnionType1 | ADiscriminatedUnionType2 | None | Unset, data) discriminated_union = _parse_discriminated_union(d.pop("discriminated_union", UNSET)) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_merged_properties.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_merged_properties.py similarity index 72% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_merged_properties.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_merged_properties.py index 765d107d8..1ee8b4a61 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_merged_properties.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_merged_properties.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import datetime -from typing import Any, TypeVar, Union +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field -from dateutil.parser import isoparse from ..models.model_with_merged_properties_string_to_enum import ModelWithMergedPropertiesStringToEnum from ..types import UNSET, Unset @@ -15,29 +17,29 @@ class ModelWithMergedProperties: """ Attributes: - simple_string (Union[Unset, str]): extended simpleString description Default: 'new default'. - string_to_enum (Union[Unset, ModelWithMergedPropertiesStringToEnum]): Default: + simple_string (str | Unset): extended simpleString description Default: 'new default'. + string_to_enum (ModelWithMergedPropertiesStringToEnum | Unset): Default: ModelWithMergedPropertiesStringToEnum.A. - string_to_date (Union[Unset, datetime.date]): - number_to_int (Union[Unset, int]): - any_to_string (Union[Unset, str]): Default: 'x'. + string_to_date (datetime.date | Unset): + number_to_int (int | Unset): + any_to_string (str | Unset): Default: 'x'. """ - simple_string: Union[Unset, str] = "new default" - string_to_enum: Union[Unset, ModelWithMergedPropertiesStringToEnum] = ModelWithMergedPropertiesStringToEnum.A - string_to_date: Union[Unset, datetime.date] = UNSET - number_to_int: Union[Unset, int] = UNSET - any_to_string: Union[Unset, str] = "x" + simple_string: str | Unset = "new default" + string_to_enum: ModelWithMergedPropertiesStringToEnum | Unset = ModelWithMergedPropertiesStringToEnum.A + string_to_date: datetime.date | Unset = UNSET + number_to_int: int | Unset = UNSET + any_to_string: str | Unset = "x" additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: simple_string = self.simple_string - string_to_enum: Union[Unset, str] = UNSET + string_to_enum: str | Unset = UNSET if not isinstance(self.string_to_enum, Unset): string_to_enum = self.string_to_enum.value - string_to_date: Union[Unset, str] = UNSET + string_to_date: str | Unset = UNSET if not isinstance(self.string_to_date, Unset): string_to_date = self.string_to_date.isoformat() @@ -62,23 +64,23 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) simple_string = d.pop("simpleString", UNSET) _string_to_enum = d.pop("stringToEnum", UNSET) - string_to_enum: Union[Unset, ModelWithMergedPropertiesStringToEnum] + string_to_enum: ModelWithMergedPropertiesStringToEnum | Unset if isinstance(_string_to_enum, Unset): string_to_enum = UNSET else: string_to_enum = ModelWithMergedPropertiesStringToEnum(_string_to_enum) _string_to_date = d.pop("stringToDate", UNSET) - string_to_date: Union[Unset, datetime.date] + string_to_date: datetime.date | Unset if isinstance(_string_to_date, Unset): string_to_date = UNSET else: - string_to_date = isoparse(_string_to_date).date() + string_to_date = datetime.date.fromisoformat(_string_to_date) number_to_int = d.pop("numberToInt", UNSET) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_merged_properties_string_to_enum.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_merged_properties_string_to_enum.py similarity index 52% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_merged_properties_string_to_enum.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_merged_properties_string_to_enum.py index 5e146c5eb..22fb32812 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_merged_properties_string_to_enum.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_merged_properties_string_to_enum.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class ModelWithMergedPropertiesStringToEnum(str, Enum): +class ModelWithMergedPropertiesStringToEnum(StrEnum): A = "a" B = "b" diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_no_properties.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_no_properties.py similarity index 73% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_no_properties.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_no_properties.py index 24e718e6d..32eba322b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_no_properties.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_no_properties.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -7,15 +10,14 @@ @_attrs_define class ModelWithNoProperties: - """ """ - def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: model_with_no_properties = cls() return model_with_no_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_primitive_additional_properties.py similarity index 77% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_primitive_additional_properties.py index db13972e9..f4d01fd3f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_primitive_additional_properties.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -18,14 +21,14 @@ class ModelWithPrimitiveAdditionalProperties: """ Attributes: - a_date_holder (Union[Unset, ModelWithPrimitiveAdditionalPropertiesADateHolder]): + a_date_holder (ModelWithPrimitiveAdditionalPropertiesADateHolder | Unset): """ - a_date_holder: Union[Unset, "ModelWithPrimitiveAdditionalPropertiesADateHolder"] = UNSET + a_date_holder: ModelWithPrimitiveAdditionalPropertiesADateHolder | Unset = UNSET additional_properties: dict[str, str] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - a_date_holder: Union[Unset, dict[str, Any]] = UNSET + a_date_holder: dict[str, Any] | Unset = UNSET if not isinstance(self.a_date_holder, Unset): a_date_holder = self.a_date_holder.to_dict() @@ -38,14 +41,14 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.model_with_primitive_additional_properties_a_date_holder import ( - ModelWithPrimitiveAdditionalPropertiesADateHolder, + ModelWithPrimitiveAdditionalPropertiesADateHolder, # noqa: PLC0415 ) - d = src_dict.copy() + d = dict(src_dict) _a_date_holder = d.pop("a_date_holder", UNSET) - a_date_holder: Union[Unset, ModelWithPrimitiveAdditionalPropertiesADateHolder] + a_date_holder: ModelWithPrimitiveAdditionalPropertiesADateHolder | Unset if isinstance(_a_date_holder, Unset): a_date_holder = UNSET else: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py similarity index 86% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py index f53f968ac..f5a15805d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_primitive_additional_properties_a_date_holder.py @@ -1,20 +1,21 @@ +from __future__ import annotations + import datetime +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field -from dateutil.parser import isoparse T = TypeVar("T", bound="ModelWithPrimitiveAdditionalPropertiesADateHolder") @_attrs_define class ModelWithPrimitiveAdditionalPropertiesADateHolder: - """ """ - additional_properties: dict[str, datetime.datetime] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.isoformat() @@ -22,13 +23,13 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) model_with_primitive_additional_properties_a_date_holder = cls() additional_properties = {} for prop_name, prop_dict in d.items(): - additional_property = isoparse(prop_dict) + additional_property = datetime.datetime.fromisoformat(prop_dict) additional_properties[prop_name] = additional_property diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_property_ref.py similarity index 78% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_property_ref.py index d8ef017e0..622c94fa4 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_property_ref.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -16,14 +19,14 @@ class ModelWithPropertyRef: """ Attributes: - inner (Union[Unset, ModelName]): + inner (ModelName | Unset): """ - inner: Union[Unset, "ModelName"] = UNSET + inner: ModelName | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - inner: Union[Unset, dict[str, Any]] = UNSET + inner: dict[str, Any] | Unset = UNSET if not isinstance(self.inner, Unset): inner = self.inner.to_dict() @@ -36,12 +39,12 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - from ..models.model_name import ModelName + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.model_name import ModelName # noqa: PLC0415 - d = src_dict.copy() + d = dict(src_dict) _inner = d.pop("inner", UNSET) - inner: Union[Unset, ModelName] + inner: ModelName | Unset if isinstance(_inner, Unset): inner = UNSET else: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_recursive_ref.py similarity index 80% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_recursive_ref.py index f7370d6f1..d4149434c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_recursive_ref.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,14 +15,14 @@ class ModelWithRecursiveRef: """ Attributes: - recursive (Union[Unset, ModelWithRecursiveRef]): + recursive (ModelWithRecursiveRef | Unset): """ - recursive: Union[Unset, "ModelWithRecursiveRef"] = UNSET + recursive: ModelWithRecursiveRef | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - recursive: Union[Unset, dict[str, Any]] = UNSET + recursive: dict[str, Any] | Unset = UNSET if not isinstance(self.recursive, Unset): recursive = self.recursive.to_dict() @@ -32,10 +35,10 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) _recursive = d.pop("recursive", UNSET) - recursive: Union[Unset, ModelWithRecursiveRef] + recursive: ModelWithRecursiveRef | Unset if isinstance(_recursive, Unset): recursive = UNSET else: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py similarity index 76% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py index 961b82697..f31d2f88d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_recursive_ref_in_additional_properties.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -8,13 +11,12 @@ @_attrs_define class ModelWithRecursiveRefInAdditionalProperties: - """ """ - - additional_properties: dict[str, "ModelWithRecursiveRefInAdditionalProperties"] = _attrs_field( + additional_properties: dict[str, ModelWithRecursiveRefInAdditionalProperties] = _attrs_field( init=False, factory=dict ) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} for prop_name, prop in self.additional_properties.items(): field_dict[prop_name] = prop.to_dict() @@ -22,8 +24,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) model_with_recursive_ref_in_additional_properties = cls() additional_properties = {} @@ -39,10 +41,10 @@ def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: def additional_keys(self) -> list[str]: return list(self.additional_properties.keys()) - def __getitem__(self, key: str) -> "ModelWithRecursiveRefInAdditionalProperties": + def __getitem__(self, key: str) -> ModelWithRecursiveRefInAdditionalProperties: return self.additional_properties[key] - def __setitem__(self, key: str, value: "ModelWithRecursiveRefInAdditionalProperties") -> None: + def __setitem__(self, key: str, value: ModelWithRecursiveRefInAdditionalProperties) -> None: self.additional_properties[key] = value def __delitem__(self, key: str) -> None: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property.py similarity index 74% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property.py index e818fc69b..176b3eadc 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define @@ -13,13 +16,13 @@ class ModelWithUnionProperty: """ Attributes: - a_property (Union[AnEnum, AnIntEnum, Unset]): + a_property (AnEnum | AnIntEnum | Unset): """ - a_property: Union[AnEnum, AnIntEnum, Unset] = UNSET + a_property: AnEnum | AnIntEnum | Unset = UNSET def to_dict(self) -> dict[str, Any]: - a_property: Union[Unset, int, str] + a_property: int | str | Unset if isinstance(self.a_property, Unset): a_property = UNSET elif isinstance(self.a_property, AnEnum): @@ -28,6 +31,7 @@ def to_dict(self) -> dict[str, Any]: a_property = self.a_property.value field_dict: dict[str, Any] = {} + field_dict.update({}) if a_property is not UNSET: field_dict["a_property"] = a_property @@ -35,10 +39,10 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) - def _parse_a_property(data: object) -> Union[AnEnum, AnIntEnum, Unset]: + def _parse_a_property(data: object) -> AnEnum | AnIntEnum | Unset: if isinstance(data, Unset): return data try: @@ -47,7 +51,7 @@ def _parse_a_property(data: object) -> Union[AnEnum, AnIntEnum, Unset]: a_property_type_0 = AnEnum(data) return a_property_type_0 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass if not isinstance(data, int): raise TypeError() diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined.py new file mode 100644 index 000000000..3a4f5b45e --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.model_with_union_property_inlined_apples import ModelWithUnionPropertyInlinedApples + from ..models.model_with_union_property_inlined_bananas import ModelWithUnionPropertyInlinedBananas + + +T = TypeVar("T", bound="ModelWithUnionPropertyInlined") + + +@_attrs_define +class ModelWithUnionPropertyInlined: + """ + Attributes: + fruit (ModelWithUnionPropertyInlinedApples | ModelWithUnionPropertyInlinedBananas | Unset): + """ + + fruit: ModelWithUnionPropertyInlinedApples | ModelWithUnionPropertyInlinedBananas | Unset = UNSET + + def to_dict(self) -> dict[str, Any]: + from ..models.model_with_union_property_inlined_apples import ModelWithUnionPropertyInlinedApples # noqa: PLC0415 + + fruit: dict[str, Any] | Unset + if isinstance(self.fruit, Unset): + fruit = UNSET + elif isinstance(self.fruit, ModelWithUnionPropertyInlinedApples): + fruit = self.fruit.to_dict() + else: + fruit = self.fruit.to_dict() + + field_dict: dict[str, Any] = {} + + field_dict.update({}) + if fruit is not UNSET: + field_dict["fruit"] = fruit + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.model_with_union_property_inlined_apples import ModelWithUnionPropertyInlinedApples # noqa: PLC0415 + from ..models.model_with_union_property_inlined_bananas import ModelWithUnionPropertyInlinedBananas # noqa: PLC0415 + + d = dict(src_dict) + + def _parse_fruit( + data: object, + ) -> ModelWithUnionPropertyInlinedApples | ModelWithUnionPropertyInlinedBananas | Unset: + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + fruit_apples = ModelWithUnionPropertyInlinedApples.from_dict(data) + + return fruit_apples + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + fruit_bananas = ModelWithUnionPropertyInlinedBananas.from_dict(data) + + return fruit_bananas + + fruit = _parse_fruit(d.pop("fruit", UNSET)) + + model_with_union_property_inlined = cls( + fruit=fruit, + ) + + return model_with_union_property_inlined diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_fruit_type_0.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined_apples.py similarity index 67% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_fruit_type_0.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined_apples.py index 4678b4cef..0ad6f0cf2 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_fruit_type_0.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined_apples.py @@ -1,21 +1,24 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field from ..types import UNSET, Unset -T = TypeVar("T", bound="ModelWithUnionPropertyInlinedFruitType0") +T = TypeVar("T", bound="ModelWithUnionPropertyInlinedApples") @_attrs_define -class ModelWithUnionPropertyInlinedFruitType0: +class ModelWithUnionPropertyInlinedApples: """ Attributes: - apples (Union[Unset, str]): + apples (str | Unset): """ - apples: Union[Unset, str] = UNSET + apples: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -30,16 +33,16 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) apples = d.pop("apples", UNSET) - model_with_union_property_inlined_fruit_type_0 = cls( + model_with_union_property_inlined_apples = cls( apples=apples, ) - model_with_union_property_inlined_fruit_type_0.additional_properties = d - return model_with_union_property_inlined_fruit_type_0 + model_with_union_property_inlined_apples.additional_properties = d + return model_with_union_property_inlined_apples @property def additional_keys(self) -> list[str]: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_fruit_type_1.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined_bananas.py similarity index 67% rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_fruit_type_1.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined_bananas.py index d70e54234..68a54e760 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_union_property_inlined_fruit_type_1.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/model_with_union_property_inlined_bananas.py @@ -1,21 +1,24 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field from ..types import UNSET, Unset -T = TypeVar("T", bound="ModelWithUnionPropertyInlinedFruitType1") +T = TypeVar("T", bound="ModelWithUnionPropertyInlinedBananas") @_attrs_define -class ModelWithUnionPropertyInlinedFruitType1: +class ModelWithUnionPropertyInlinedBananas: """ Attributes: - bananas (Union[Unset, str]): + bananas (str | Unset): """ - bananas: Union[Unset, str] = UNSET + bananas: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -30,16 +33,16 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) bananas = d.pop("bananas", UNSET) - model_with_union_property_inlined_fruit_type_1 = cls( + model_with_union_property_inlined_bananas = cls( bananas=bananas, ) - model_with_union_property_inlined_fruit_type_1.additional_properties = d - return model_with_union_property_inlined_fruit_type_1 + model_with_union_property_inlined_bananas.additional_properties = d + return model_with_union_property_inlined_bananas @property def additional_keys(self) -> list[str]: diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/none.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/none.py similarity index 86% rename from end_to_end_tests/golden-record/my_test_api_client/models/none.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/none.py index 23cb7d679..6f7fa6047 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/none.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/none.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -8,19 +11,18 @@ @_attrs_define class None_: - """ """ - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) none = cls() none.additional_properties = d diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/octet_stream_tests_octet_stream_post_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/octet_stream_tests_octet_stream_post_response_200.py new file mode 100644 index 000000000..8d138937a --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/octet_stream_tests_octet_stream_post_response_200.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Mapping +from io import BytesIO +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, File, FileTypes, Unset + +T = TypeVar("T", bound="OctetStreamTestsOctetStreamPostResponse200") + + +@_attrs_define +class OctetStreamTestsOctetStreamPostResponse200: + """ + Attributes: + data (File | Unset): + """ + + data: File | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + data: FileTypes | Unset = UNSET + if not isinstance(self.data, Unset): + data = self.data.to_tuple() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if data is not UNSET: + field_dict["data"] = data + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + _data = d.pop("data", UNSET) + data: File | Unset + if isinstance(_data, Unset): + data = UNSET + else: + data = File(payload=BytesIO(_data)) + + octet_stream_tests_octet_stream_post_response_200 = cls( + data=data, + ) + + octet_stream_tests_octet_stream_post_response_200.additional_properties = d + return octet_stream_tests_octet_stream_post_response_200 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/optional_body_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/optional_body_body.py new file mode 100644 index 000000000..064beb792 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/optional_body_body.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="OptionalBodyBody") + + +@_attrs_define +class OptionalBodyBody: + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + optional_body_body = cls() + + optional_body_body.additional_properties = d + return optional_body_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_data_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_data_body.py similarity index 84% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_data_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_data_body.py index ba36efef2..d83f8d7eb 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_data_body.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_data_body.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,10 +15,10 @@ class PostBodiesMultipleDataBody: """ Attributes: - a (Union[Unset, str]): + a (str | Unset): """ - a: Union[Unset, str] = UNSET + a: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -30,8 +33,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) a = d.pop("a", UNSET) post_bodies_multiple_data_body = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_files_body.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_files_body.py index 188abf39e..81d7db7a7 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_files_body.py @@ -1,8 +1,12 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field +from .. import types from ..types import UNSET, Unset T = TypeVar("T", bound="PostBodiesMultipleFilesBody") @@ -12,10 +16,10 @@ class PostBodiesMultipleFilesBody: """ Attributes: - a (Union[Unset, str]): + a (str | Unset): """ - a: Union[Unset, str] = UNSET + a: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -29,22 +33,20 @@ def to_dict(self) -> dict[str, Any]: return field_dict - def to_multipart(self) -> dict[str, Any]: - a = self.a if isinstance(self.a, Unset) else (None, str(self.a).encode(), "text/plain") + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] - field_dict: dict[str, Any] = {} - for prop_name, prop in self.additional_properties.items(): - field_dict[prop_name] = (None, str(prop).encode(), "text/plain") + if not isinstance(self.a, Unset): + files.append(("a", (None, str(self.a).encode(), "text/plain"))) - field_dict.update({}) - if a is not UNSET: - field_dict["a"] = a + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) - return field_dict + return files @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) a = d.pop("a", UNSET) post_bodies_multiple_files_body = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_json_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_json_body.py similarity index 84% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_json_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_json_body.py index f4e7ffaa6..a5ec152e9 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_json_body.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_bodies_multiple_json_body.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,10 +15,10 @@ class PostBodiesMultipleJsonBody: """ Attributes: - a (Union[Unset, str]): + a (str | Unset): """ - a: Union[Unset, str] = UNSET + a: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -30,8 +33,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) a = d.pop("a", UNSET) post_bodies_multiple_json_body = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_form_data_inline_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_form_data_inline_body.py similarity index 86% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_form_data_inline_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_form_data_inline_body.py index 07c3b2648..5d7c3982b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_form_data_inline_body.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_form_data_inline_body.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -13,11 +16,11 @@ class PostFormDataInlineBody: """ Attributes: a_required_field (str): - an_optional_field (Union[Unset, str]): + an_optional_field (str | Unset): """ a_required_field: str - an_optional_field: Union[Unset, str] = UNSET + an_optional_field: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -38,8 +41,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) a_required_field = d.pop("a_required_field") an_optional_field = d.pop("an_optional_field", UNSET) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_naming_property_conflict_with_import_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_naming_property_conflict_with_import_body.py similarity index 78% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_naming_property_conflict_with_import_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_naming_property_conflict_with_import_body.py index 100e84dc9..2895d97d8 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_naming_property_conflict_with_import_body.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_naming_property_conflict_with_import_body.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,12 +15,12 @@ class PostNamingPropertyConflictWithImportBody: """ Attributes: - field (Union[Unset, str]): A python_name of field should not interfere with attrs field - define (Union[Unset, str]): A python_name of define should not interfere with attrs define + field (str | Unset): A python_name of field should not interfere with attrs field + define (str | Unset): A python_name of define should not interfere with attrs define """ - field: Union[Unset, str] = UNSET - define: Union[Unset, str] = UNSET + field: str | Unset = UNSET + define: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -36,8 +39,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) field = d.pop("Field", UNSET) define = d.pop("Define", UNSET) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_naming_property_conflict_with_import_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_naming_property_conflict_with_import_response_200.py similarity index 78% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_naming_property_conflict_with_import_response_200.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_naming_property_conflict_with_import_response_200.py index 91c550749..c2a8ac7f3 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_naming_property_conflict_with_import_response_200.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_naming_property_conflict_with_import_response_200.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,12 +15,12 @@ class PostNamingPropertyConflictWithImportResponse200: """ Attributes: - field (Union[Unset, str]): A python_name of field should not interfere with attrs field - define (Union[Unset, str]): A python_name of define should not interfere with attrs define + field (str | Unset): A python_name of field should not interfere with attrs field + define (str | Unset): A python_name of define should not interfere with attrs define """ - field: Union[Unset, str] = UNSET - define: Union[Unset, str] = UNSET + field: str | Unset = UNSET + define: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -36,8 +39,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) field = d.pop("Field", UNSET) define = d.pop("Define", UNSET) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py similarity index 77% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py index 9962f552c..205a1cd30 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union, cast +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -16,18 +19,18 @@ class PostResponsesUnionsSimpleBeforeComplexResponse200: """ Attributes: - a (Union['PostResponsesUnionsSimpleBeforeComplexResponse200AType1', str]): + a (PostResponsesUnionsSimpleBeforeComplexResponse200AType1 | str): """ - a: Union["PostResponsesUnionsSimpleBeforeComplexResponse200AType1", str] + a: PostResponsesUnionsSimpleBeforeComplexResponse200AType1 | str additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.post_responses_unions_simple_before_complex_response_200a_type_1 import ( - PostResponsesUnionsSimpleBeforeComplexResponse200AType1, + PostResponsesUnionsSimpleBeforeComplexResponse200AType1, # noqa: PLC0415 ) - a: Union[dict[str, Any], str] + a: dict[str, Any] | str if isinstance(self.a, PostResponsesUnionsSimpleBeforeComplexResponse200AType1): a = self.a.to_dict() else: @@ -44,23 +47,23 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.post_responses_unions_simple_before_complex_response_200a_type_1 import ( - PostResponsesUnionsSimpleBeforeComplexResponse200AType1, + PostResponsesUnionsSimpleBeforeComplexResponse200AType1, # noqa: PLC0415 ) - d = src_dict.copy() + d = dict(src_dict) - def _parse_a(data: object) -> Union["PostResponsesUnionsSimpleBeforeComplexResponse200AType1", str]: + def _parse_a(data: object) -> PostResponsesUnionsSimpleBeforeComplexResponse200AType1 | str: try: if not isinstance(data, dict): raise TypeError() a_type_1 = PostResponsesUnionsSimpleBeforeComplexResponse200AType1.from_dict(data) return a_type_1 - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass - return cast(Union["PostResponsesUnionsSimpleBeforeComplexResponse200AType1", str], data) + return cast(PostResponsesUnionsSimpleBeforeComplexResponse200AType1 | str, data) a = _parse_a(d.pop("a")) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py similarity index 88% rename from end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py index 5e8ef0207..2b6169b11 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -8,19 +11,18 @@ @_attrs_define class PostResponsesUnionsSimpleBeforeComplexResponse200AType1: - """ """ - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) post_responses_unions_simple_before_complex_response_200a_type_1 = cls() post_responses_unions_simple_before_complex_response_200a_type_1.additional_properties = d diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_2xx.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_2xx.py new file mode 100644 index 000000000..61e806ff9 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_2xx.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.status_code_patterns_response_2xx_status import StatusCodePatternsResponse2XXStatus +from ..types import UNSET, Unset + +T = TypeVar("T", bound="StatusCodePatternsResponse2XX") + + +@_attrs_define +class StatusCodePatternsResponse2XX: + """ + Attributes: + status (StatusCodePatternsResponse2XXStatus | Unset): + """ + + status: StatusCodePatternsResponse2XXStatus | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + status: str | Unset = UNSET + if not isinstance(self.status, Unset): + status = self.status.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if status is not UNSET: + field_dict["status"] = status + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + _status = d.pop("status", UNSET) + status: StatusCodePatternsResponse2XXStatus | Unset + if isinstance(_status, Unset): + status = UNSET + else: + status = StatusCodePatternsResponse2XXStatus(_status) + + status_code_patterns_response_2xx = cls( + status=status, + ) + + status_code_patterns_response_2xx.additional_properties = d + return status_code_patterns_response_2xx + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_2xx_status.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_2xx_status.py new file mode 100644 index 000000000..9e871d06d --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_2xx_status.py @@ -0,0 +1,9 @@ +from enum import StrEnum + + +class StatusCodePatternsResponse2XXStatus(StrEnum): + FAILURE = "failure" + SUCCESS = "success" + + def __str__(self) -> str: + return str(self.value) diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_4xx.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_4xx.py new file mode 100644 index 000000000..f62c4168a --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/status_code_patterns_response_4xx.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="StatusCodePatternsResponse4XX") + + +@_attrs_define +class StatusCodePatternsResponse4XX: + """ + Attributes: + error (str | Unset): + """ + + error: str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + error = self.error + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if error is not UNSET: + field_dict["error"] = error + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + error = d.pop("error", UNSET) + + status_code_patterns_response_4xx = cls( + error=error, + ) + + status_code_patterns_response_4xx.additional_properties = d + return status_code_patterns_response_4xx + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_body.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/test_inline_objects_body.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_body.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/test_inline_objects_body.py index 721011eed..73b054b24 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_body.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/test_inline_objects_body.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define @@ -11,15 +14,16 @@ class TestInlineObjectsBody: """ Attributes: - a_property (Union[Unset, str]): + a_property (str | Unset): """ - a_property: Union[Unset, str] = UNSET + a_property: str | Unset = UNSET def to_dict(self) -> dict[str, Any]: a_property = self.a_property field_dict: dict[str, Any] = {} + field_dict.update({}) if a_property is not UNSET: field_dict["a_property"] = a_property @@ -27,8 +31,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) a_property = d.pop("a_property", UNSET) test_inline_objects_body = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_response_200.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/test_inline_objects_response_200.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_response_200.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/test_inline_objects_response_200.py index 2cf353587..174a5957b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objects_response_200.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/test_inline_objects_response_200.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define @@ -11,15 +14,16 @@ class TestInlineObjectsResponse200: """ Attributes: - a_property (Union[Unset, str]): + a_property (str | Unset): """ - a_property: Union[Unset, str] = UNSET + a_property: str | Unset = UNSET def to_dict(self) -> dict[str, Any]: a_property = self.a_property field_dict: dict[str, Any] = {} + field_dict.update({}) if a_property is not UNSET: field_dict["a_property"] = a_property @@ -27,8 +31,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) a_property = d.pop("a_property", UNSET) test_inline_objects_response_200 = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/validation_error.py similarity index 84% rename from end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py rename to end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/validation_error.py index 112808e62..9bb9070d6 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/models/validation_error.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar, cast from attrs import define as _attrs_define @@ -26,6 +29,7 @@ def to_dict(self) -> dict[str, Any]: type_ = self.type_ field_dict: dict[str, Any] = {} + field_dict.update( { "loc": loc, @@ -37,8 +41,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) loc = cast(list[str], d.pop("loc")) msg = d.pop("msg") diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/py.typed b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/py.typed new file mode 100644 index 000000000..1aad32711 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561 \ No newline at end of file diff --git a/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/types.py b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/types.py new file mode 100644 index 000000000..9090a2620 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/my_test_api_client/types.py @@ -0,0 +1,54 @@ +"""Contains some shared types for properties""" + +from collections.abc import Mapping, MutableMapping +from http import HTTPStatus +from typing import IO, BinaryIO, Generic, Literal, TypeVar + +from attrs import define + + +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + +# The types that `httpx2.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] + + +@define +class File: + """Contains information for file uploads""" + + payload: BinaryIO + file_name: str | None = None + mime_type: str | None = None + + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx2 will accept for multipart/form-data""" + return self.file_name, self.payload, self.mime_type + + +T = TypeVar("T") + + +@define +class Response(Generic[T]): + """A response from an endpoint""" + + status_code: HTTPStatus + content: bytes + headers: MutableMapping[str, str] + parsed: T | None + + +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/end_to_end_tests/golden-records/my-test-api-client/pyproject.toml b/end_to_end_tests/golden-records/my-test-api-client/pyproject.toml new file mode 100644 index 000000000..0b22c3334 --- /dev/null +++ b/end_to_end_tests/golden-records/my-test-api-client/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "my-test-api-client" +version = "0.1.0" +description = "A client library for accessing My Test API" +authors = [] +readme = "README.md" +packages = [ + { include = "my_test_api_client" }, +] +include = ["my_test_api_client/py.typed"] + +[tool.poetry.dependencies] +python = "^3.11" +httpx2 = ">=2.13.1,<3" +attrs = ">=22.2.0" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = ["F", "I", "UP"] diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/.gitignore b/end_to_end_tests/golden-records/test-3-1-features-client/.gitignore new file mode 100644 index 000000000..79a2c3d73 --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/.gitignore @@ -0,0 +1,23 @@ +__pycache__/ +build/ +dist/ +*.egg-info/ +.pytest_cache/ + +# pyenv +.python-version + +# Environments +.env +.venv + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# JetBrains +.idea/ + +/coverage.xml +/.coverage diff --git a/end_to_end_tests/test-3-1-golden-record/README.md b/end_to_end_tests/golden-records/test-3-1-features-client/README.md similarity index 88% rename from end_to_end_tests/test-3-1-golden-record/README.md rename to end_to_end_tests/golden-records/test-3-1-features-client/README.md index dbe8a5c1e..5a1b530f6 100644 --- a/end_to_end_tests/test-3-1-golden-record/README.md +++ b/end_to_end_tests/golden-records/test-3-1-features-client/README.md @@ -47,7 +47,7 @@ By default, when you're calling an HTTPS API it will attempt to verify that SSL ```python client = AuthenticatedClient( - base_url="https://internal_api.example.com", + base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl="/path/to/certificate_bundle.pem", ) @@ -56,11 +56,7 @@ client = AuthenticatedClient( You can also disable certificate validation altogether, but beware that **this is a security risk**. ```python -client = AuthenticatedClient( - base_url="https://internal_api.example.com", - token="SuperSecretToken", - verify_ssl=False -) +client = AuthenticatedClient(base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl=False) ``` Things to know: @@ -76,37 +72,40 @@ Things to know: ## Advanced customizations -There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx.Client` or `httpx.AsyncClient` (depending on your use-case): +There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx2.Client` or `httpx2.AsyncClient` (depending on your use-case): ```python from test_3_1_features_client import Client + def log_request(request): print(f"Request event hook: {request.method} {request.url} - Waiting for response") + def log_response(response): request = response.request print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") + client = Client( base_url="https://api.example.com", httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}}, ) -# Or get the underlying httpx client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() +# Or get the underlying httpx2 client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() ``` -You can even set the httpx client directly, but beware that this will override any existing settings (e.g., base_url): +You can even set the httpx2 client directly, but beware that this will override any existing settings (e.g., base_url): ```python -import httpx +import httpx2 from test_3_1_features_client import Client client = Client( base_url="https://api.example.com", ) # Note that base_url needs to be re-set, as would any shared cookies, headers, etc. -client.set_httpx_client(httpx.Client(base_url="https://api.example.com", proxies="http://localhost:8030")) +client.set_httpx_client(httpx2.Client(base_url="https://api.example.com", proxy="http://localhost:8030")) ``` ## Building / publishing this package diff --git a/end_to_end_tests/test-3-1-golden-record/pyproject.toml b/end_to_end_tests/golden-records/test-3-1-features-client/pyproject.toml similarity index 62% rename from end_to_end_tests/test-3-1-golden-record/pyproject.toml rename to end_to_end_tests/golden-records/test-3-1-features-client/pyproject.toml index 2e8cd6c04..e92a71149 100644 --- a/end_to_end_tests/test-3-1-golden-record/pyproject.toml +++ b/end_to_end_tests/golden-records/test-3-1-features-client/pyproject.toml @@ -5,19 +5,17 @@ description = "A client library for accessing Test 3.1 Features" authors = [] readme = "README.md" packages = [ - {include = "test_3_1_features_client"}, + { include = "test_3_1_features_client" }, ] -include = ["CHANGELOG.md", "test_3_1_features_client/py.typed"] - +include = ["test_3_1_features_client/py.typed"] [tool.poetry.dependencies] -python = "^3.9" -httpx = ">=0.20.0,<0.29.0" +python = "^3.11" +httpx2 = ">=2.13.1,<3" attrs = ">=22.2.0" -python-dateutil = "^2.8.0" [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.ruff] diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/__init__.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/__init__.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/__init__.py rename to end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/__init__.py diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/__init__.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/__init__.py new file mode 100644 index 000000000..81f9fa241 --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/__init__.py @@ -0,0 +1 @@ +"""Contains methods for accessing the API""" diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/__init__.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/post_const_path.py similarity index 73% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py rename to end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/post_const_path.py index 0d05b8189..dcdf89e9d 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/const/post_const_path.py @@ -1,7 +1,8 @@ from http import HTTPStatus -from typing import Any, Literal, Optional, Union, cast +from typing import Any, Literal, cast +from urllib.parse import quote -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -14,7 +15,7 @@ def _get_kwargs( *, body: PostConstPathBody, required_query: Literal["this always goes in the query"], - optional_query: Union[Literal["this sometimes goes in the query"], Unset] = UNSET, + optional_query: Literal["this sometimes goes in the query"] | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -28,13 +29,14 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "post", - "url": f"/const/{path}", + "url": "/const/{path}".format( + path=quote(str(path), safe=""), + ), "params": params, } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers @@ -42,8 +44,8 @@ def _get_kwargs( def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Literal["Why have a fixed response? I dunno"]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Literal["Why have a fixed response? I dunno"] | None: if response.status_code == 200: response_200 = cast(Literal["Why have a fixed response? I dunno"], response.json()) if response_200 != "Why have a fixed response? I dunno": @@ -51,6 +53,7 @@ def _parse_response( f"response_200 must match const 'Why have a fixed response? I dunno', got '{response_200}'" ) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -58,7 +61,7 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response + *, client: AuthenticatedClient | Client, response: httpx2.Response ) -> Response[Literal["Why have a fixed response? I dunno"]]: return Response( status_code=HTTPStatus(response.status_code), @@ -71,21 +74,21 @@ def _build_response( def sync_detailed( path: Literal["this goes in the path"], *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: PostConstPathBody, required_query: Literal["this always goes in the query"], - optional_query: Union[Literal["this sometimes goes in the query"], Unset] = UNSET, + optional_query: Literal["this sometimes goes in the query"] | Unset = UNSET, ) -> Response[Literal["Why have a fixed response? I dunno"]]: """ Args: path (Literal['this goes in the path']): required_query (Literal['this always goes in the query']): - optional_query (Union[Literal['this sometimes goes in the query'], Unset]): + optional_query (Literal['this sometimes goes in the query'] | Unset): body (PostConstPathBody): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Literal['Why have a fixed response? I dunno']] @@ -108,21 +111,21 @@ def sync_detailed( def sync( path: Literal["this goes in the path"], *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: PostConstPathBody, required_query: Literal["this always goes in the query"], - optional_query: Union[Literal["this sometimes goes in the query"], Unset] = UNSET, -) -> Optional[Literal["Why have a fixed response? I dunno"]]: + optional_query: Literal["this sometimes goes in the query"] | Unset = UNSET, +) -> Literal["Why have a fixed response? I dunno"] | None: """ Args: path (Literal['this goes in the path']): required_query (Literal['this always goes in the query']): - optional_query (Union[Literal['this sometimes goes in the query'], Unset]): + optional_query (Literal['this sometimes goes in the query'] | Unset): body (PostConstPathBody): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Literal['Why have a fixed response? I dunno'] @@ -140,21 +143,21 @@ def sync( async def asyncio_detailed( path: Literal["this goes in the path"], *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: PostConstPathBody, required_query: Literal["this always goes in the query"], - optional_query: Union[Literal["this sometimes goes in the query"], Unset] = UNSET, + optional_query: Literal["this sometimes goes in the query"] | Unset = UNSET, ) -> Response[Literal["Why have a fixed response? I dunno"]]: """ Args: path (Literal['this goes in the path']): required_query (Literal['this always goes in the query']): - optional_query (Union[Literal['this sometimes goes in the query'], Unset]): + optional_query (Literal['this sometimes goes in the query'] | Unset): body (PostConstPathBody): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[Literal['Why have a fixed response? I dunno']] @@ -175,21 +178,21 @@ async def asyncio_detailed( async def asyncio( path: Literal["this goes in the path"], *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: PostConstPathBody, required_query: Literal["this always goes in the query"], - optional_query: Union[Literal["this sometimes goes in the query"], Unset] = UNSET, -) -> Optional[Literal["Why have a fixed response? I dunno"]]: + optional_query: Literal["this sometimes goes in the query"] | Unset = UNSET, +) -> Literal["Why have a fixed response? I dunno"] | None: """ Args: path (Literal['this goes in the path']): required_query (Literal['this always goes in the query']): - optional_query (Union[Literal['this sometimes goes in the query'], Unset]): + optional_query (Literal['this sometimes goes in the query'] | Unset): body (PostConstPathBody): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Literal['Why have a fixed response? I dunno'] diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/__init__.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/post_prefix_items.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/post_prefix_items.py similarity index 75% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/post_prefix_items.py rename to end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/post_prefix_items.py index f12d53f18..d20bd99ce 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/post_prefix_items.py +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/api/prefix_items/post_prefix_items.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union, cast +from typing import Any, cast -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -20,26 +20,26 @@ def _get_kwargs( "url": "/prefixItems", } - _body = body.to_dict() + _kwargs["json"] = body.to_dict() - _kwargs["json"] = _body headers["Content-Type"] = "application/json" _kwargs["headers"] = headers return _kwargs -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[str]: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> str | None: if response.status_code == 200: response_200 = cast(str, response.json()) return response_200 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[str]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[str]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -50,7 +50,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: PostPrefixItemsBody, ) -> Response[str]: """ @@ -59,7 +59,7 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[str] @@ -78,16 +78,16 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: PostPrefixItemsBody, -) -> Optional[str]: +) -> str | None: """ Args: body (PostPrefixItemsBody): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: str @@ -101,7 +101,7 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: PostPrefixItemsBody, ) -> Response[str]: """ @@ -110,7 +110,7 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: Response[str] @@ -127,16 +127,16 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, body: PostPrefixItemsBody, -) -> Optional[str]: +) -> str | None: """ Args: body (PostPrefixItemsBody): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: str diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/client.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/client.py new file mode 100644 index 000000000..136b83d06 --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/client.py @@ -0,0 +1,268 @@ +import ssl +from typing import Any, Self + +import httpx2 +from attrs import define, evolve, field + + +@define +class Client: + """A class for keeping track of data related to the API + + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx2.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) + + def with_headers(self, headers: dict[str, str]) -> "Client": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "Client": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx2.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" + if self._client is None: + self._client = httpx2.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._async_client = httpx2.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +@define +class AuthenticatedClient: + """A Client which has been authenticated for use on secured endpoints + + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx2.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + token: The token to use for authentication + prefix: The prefix to use for the Authorization header + auth_header_name: The name of the Authorization header + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) + + token: str + prefix: str = "Bearer" + auth_header_name: str = "Authorization" + + def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx2.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" + if self._client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._client = httpx2.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._async_client = httpx2.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/errors.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/errors.py new file mode 100644 index 000000000..5f92e76ac --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/errors.py @@ -0,0 +1,16 @@ +"""Contains shared errors types that can be raised from API functions""" + + +class UnexpectedStatus(Exception): + """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" + + def __init__(self, status_code: int, content: bytes): + self.status_code = status_code + self.content = content + + super().__init__( + f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" + ) + + +__all__ = ["UnexpectedStatus"] diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/__init__.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/__init__.py similarity index 100% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/__init__.py rename to end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/__init__.py diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/post_const_path_body.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/post_const_path_body.py similarity index 75% rename from end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/post_const_path_body.py rename to end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/post_const_path_body.py index 15734903c..f59894f08 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/post_const_path_body.py +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/post_const_path_body.py @@ -1,4 +1,7 @@ -from typing import Any, Literal, TypeVar, Union, cast +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Literal, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -13,19 +16,19 @@ class PostConstPathBody: """ Attributes: required (Literal['this always goes in the body']): - nullable (Union[Literal['this or null goes in the body'], None]): - optional (Union[Literal['this sometimes goes in the body'], Unset]): + nullable (Literal['this or null goes in the body'] | None): + optional (Literal['this sometimes goes in the body'] | Unset): """ required: Literal["this always goes in the body"] - nullable: Union[Literal["this or null goes in the body"], None] - optional: Union[Literal["this sometimes goes in the body"], Unset] = UNSET + nullable: Literal["this or null goes in the body"] | None + optional: Literal["this sometimes goes in the body"] | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: required = self.required - nullable: Union[Literal["this or null goes in the body"], None] + nullable: Literal["this or null goes in the body"] | None nullable = self.nullable optional = self.optional @@ -44,13 +47,13 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) required = cast(Literal["this always goes in the body"], d.pop("required")) if required != "this always goes in the body": raise ValueError(f"required must match const 'this always goes in the body', got '{required}'") - def _parse_nullable(data: object) -> Union[Literal["this or null goes in the body"], None]: + def _parse_nullable(data: object) -> Literal["this or null goes in the body"] | None: if data is None: return data nullable_type_1 = cast(Literal["this or null goes in the body"], data) @@ -59,11 +62,11 @@ def _parse_nullable(data: object) -> Union[Literal["this or null goes in the bod f"nullable_type_1 must match const 'this or null goes in the body', got '{nullable_type_1}'" ) return nullable_type_1 - return cast(Union[Literal["this or null goes in the body"], None], data) + return cast(Literal["this or null goes in the body"] | None, data) nullable = _parse_nullable(d.pop("nullable")) - optional = cast(Union[Literal["this sometimes goes in the body"], Unset], d.pop("optional", UNSET)) + optional = cast(Literal["this sometimes goes in the body"] | Unset, d.pop("optional", UNSET)) if optional != "this sometimes goes in the body" and not isinstance(optional, Unset): raise ValueError(f"optional must match const 'this sometimes goes in the body', got '{optional}'") diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/post_prefix_items_body.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/post_prefix_items_body.py new file mode 100644 index 000000000..b074bb717 --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/models/post_prefix_items_body.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Literal, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="PostPrefixItemsBody") + + +@_attrs_define +class PostPrefixItemsBody: + """ + Attributes: + prefix_items_and_items (list[float | Literal['prefix'] | str] | Unset): + prefix_items_only (list[float | str] | Unset): + """ + + prefix_items_and_items: list[float | Literal["prefix"] | str] | Unset = UNSET + prefix_items_only: list[float | str] | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + prefix_items_and_items: list[float | Literal["prefix"] | str] | Unset = UNSET + if not isinstance(self.prefix_items_and_items, Unset): + prefix_items_and_items = [] + for prefix_items_and_items_item_data in self.prefix_items_and_items: + prefix_items_and_items_item: float | Literal["prefix"] | str + prefix_items_and_items_item = prefix_items_and_items_item_data + prefix_items_and_items.append(prefix_items_and_items_item) + + prefix_items_only: list[float | str] | Unset = UNSET + if not isinstance(self.prefix_items_only, Unset): + prefix_items_only = [] + for prefix_items_only_item_data in self.prefix_items_only: + prefix_items_only_item: float | str + prefix_items_only_item = prefix_items_only_item_data + prefix_items_only.append(prefix_items_only_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if prefix_items_and_items is not UNSET: + field_dict["prefixItemsAndItems"] = prefix_items_and_items + if prefix_items_only is not UNSET: + field_dict["prefixItemsOnly"] = prefix_items_only + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + _prefix_items_and_items = d.pop("prefixItemsAndItems", UNSET) + prefix_items_and_items: list[float | Literal["prefix"] | str] | Unset = UNSET + if _prefix_items_and_items is not UNSET: + prefix_items_and_items = [] + for prefix_items_and_items_item_data in _prefix_items_and_items: + + def _parse_prefix_items_and_items_item(data: object) -> float | Literal["prefix"] | str: + prefix_items_and_items_item_type_0 = cast(Literal["prefix"], data) + if prefix_items_and_items_item_type_0 != "prefix": + raise ValueError( + f"prefixItemsAndItems_item_type_0 must match const 'prefix', got '{prefix_items_and_items_item_type_0}'" + ) + return prefix_items_and_items_item_type_0 + return cast(float | Literal["prefix"] | str, data) + + prefix_items_and_items_item = _parse_prefix_items_and_items_item(prefix_items_and_items_item_data) + + prefix_items_and_items.append(prefix_items_and_items_item) + + _prefix_items_only = d.pop("prefixItemsOnly", UNSET) + prefix_items_only: list[float | str] | Unset = UNSET + if _prefix_items_only is not UNSET: + prefix_items_only = [] + for prefix_items_only_item_data in _prefix_items_only: + + def _parse_prefix_items_only_item(data: object) -> float | str: + return cast(float | str, data) + + prefix_items_only_item = _parse_prefix_items_only_item(prefix_items_only_item_data) + + prefix_items_only.append(prefix_items_only_item) + + post_prefix_items_body = cls( + prefix_items_and_items=prefix_items_and_items, + prefix_items_only=prefix_items_only, + ) + + post_prefix_items_body.additional_properties = d + return post_prefix_items_body + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/py.typed b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/py.typed new file mode 100644 index 000000000..1aad32711 --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561 \ No newline at end of file diff --git a/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/types.py b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/types.py new file mode 100644 index 000000000..9090a2620 --- /dev/null +++ b/end_to_end_tests/golden-records/test-3-1-features-client/test_3_1_features_client/types.py @@ -0,0 +1,54 @@ +"""Contains some shared types for properties""" + +from collections.abc import Mapping, MutableMapping +from http import HTTPStatus +from typing import IO, BinaryIO, Generic, Literal, TypeVar + +from attrs import define + + +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + +# The types that `httpx2.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] + + +@define +class File: + """Contains information for file uploads""" + + payload: BinaryIO + file_name: str | None = None + mime_type: str | None = None + + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx2 will accept for multipart/form-data""" + return self.file_name, self.payload, self.mime_type + + +T = TypeVar("T") + + +@define +class Response(Generic[T]): + """A response from an endpoint""" + + status_code: HTTPStatus + content: bytes + headers: MutableMapping[str, str] + parsed: T | None + + +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/__init__.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/enums/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/__init__.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/api/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py deleted file mode 100644 index 5566f1b3b..000000000 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py +++ /dev/null @@ -1,255 +0,0 @@ -import json -from typing import Any, TypeVar, Union, cast - -from attrs import define as _attrs_define -from attrs import field as _attrs_field - -from ..models.an_all_of_enum import AnAllOfEnum, check_an_all_of_enum -from ..models.an_enum import AnEnum, check_an_enum -from ..models.an_enum_with_null import AnEnumWithNull, check_an_enum_with_null -from ..models.different_enum import DifferentEnum, check_different_enum -from ..types import UNSET, Unset - -T = TypeVar("T", bound="PostUserListBody") - - -@_attrs_define -class PostUserListBody: - """ - Attributes: - an_enum_value (Union[Unset, list[AnEnum]]): - an_enum_value_with_null (Union[Unset, list[Union[AnEnumWithNull, None]]]): - an_enum_value_with_only_null (Union[Unset, list[None]]): - an_allof_enum_with_overridden_default (Union[Unset, AnAllOfEnum]): Default: 'overridden_default'. - an_optional_allof_enum (Union[Unset, AnAllOfEnum]): - nested_list_of_enums (Union[Unset, list[list[DifferentEnum]]]): - """ - - an_enum_value: Union[Unset, list[AnEnum]] = UNSET - an_enum_value_with_null: Union[Unset, list[Union[AnEnumWithNull, None]]] = UNSET - an_enum_value_with_only_null: Union[Unset, list[None]] = UNSET - an_allof_enum_with_overridden_default: Union[Unset, AnAllOfEnum] = "overridden_default" - an_optional_allof_enum: Union[Unset, AnAllOfEnum] = UNSET - nested_list_of_enums: Union[Unset, list[list[DifferentEnum]]] = UNSET - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) - - def to_dict(self) -> dict[str, Any]: - an_enum_value: Union[Unset, list[str]] = UNSET - if not isinstance(self.an_enum_value, Unset): - an_enum_value = [] - for an_enum_value_item_data in self.an_enum_value: - an_enum_value_item: str = an_enum_value_item_data - an_enum_value.append(an_enum_value_item) - - an_enum_value_with_null: Union[Unset, list[Union[None, str]]] = UNSET - if not isinstance(self.an_enum_value_with_null, Unset): - an_enum_value_with_null = [] - for an_enum_value_with_null_item_data in self.an_enum_value_with_null: - an_enum_value_with_null_item: Union[None, str] - if isinstance(an_enum_value_with_null_item_data, str): - an_enum_value_with_null_item = an_enum_value_with_null_item_data - else: - an_enum_value_with_null_item = an_enum_value_with_null_item_data - an_enum_value_with_null.append(an_enum_value_with_null_item) - - an_enum_value_with_only_null: Union[Unset, list[None]] = UNSET - if not isinstance(self.an_enum_value_with_only_null, Unset): - an_enum_value_with_only_null = self.an_enum_value_with_only_null - - an_allof_enum_with_overridden_default: Union[Unset, str] = UNSET - if not isinstance(self.an_allof_enum_with_overridden_default, Unset): - an_allof_enum_with_overridden_default = self.an_allof_enum_with_overridden_default - - an_optional_allof_enum: Union[Unset, str] = UNSET - if not isinstance(self.an_optional_allof_enum, Unset): - an_optional_allof_enum = self.an_optional_allof_enum - - nested_list_of_enums: Union[Unset, list[list[str]]] = UNSET - if not isinstance(self.nested_list_of_enums, Unset): - nested_list_of_enums = [] - for nested_list_of_enums_item_data in self.nested_list_of_enums: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item: str = nested_list_of_enums_item_item_data - nested_list_of_enums_item.append(nested_list_of_enums_item_item) - - nested_list_of_enums.append(nested_list_of_enums_item) - - field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) - field_dict.update({}) - if an_enum_value is not UNSET: - field_dict["an_enum_value"] = an_enum_value - if an_enum_value_with_null is not UNSET: - field_dict["an_enum_value_with_null"] = an_enum_value_with_null - if an_enum_value_with_only_null is not UNSET: - field_dict["an_enum_value_with_only_null"] = an_enum_value_with_only_null - if an_allof_enum_with_overridden_default is not UNSET: - field_dict["an_allof_enum_with_overridden_default"] = an_allof_enum_with_overridden_default - if an_optional_allof_enum is not UNSET: - field_dict["an_optional_allof_enum"] = an_optional_allof_enum - if nested_list_of_enums is not UNSET: - field_dict["nested_list_of_enums"] = nested_list_of_enums - - return field_dict - - def to_multipart(self) -> dict[str, Any]: - an_enum_value: Union[Unset, tuple[None, bytes, str]] = UNSET - if not isinstance(self.an_enum_value, Unset): - _temp_an_enum_value = [] - for an_enum_value_item_data in self.an_enum_value: - an_enum_value_item: str = an_enum_value_item_data - _temp_an_enum_value.append(an_enum_value_item) - an_enum_value = (None, json.dumps(_temp_an_enum_value).encode(), "application/json") - - an_enum_value_with_null: Union[Unset, tuple[None, bytes, str]] = UNSET - if not isinstance(self.an_enum_value_with_null, Unset): - _temp_an_enum_value_with_null = [] - for an_enum_value_with_null_item_data in self.an_enum_value_with_null: - an_enum_value_with_null_item: Union[None, str] - if isinstance(an_enum_value_with_null_item_data, str): - an_enum_value_with_null_item = an_enum_value_with_null_item_data - else: - an_enum_value_with_null_item = an_enum_value_with_null_item_data - _temp_an_enum_value_with_null.append(an_enum_value_with_null_item) - an_enum_value_with_null = (None, json.dumps(_temp_an_enum_value_with_null).encode(), "application/json") - - an_enum_value_with_only_null: Union[Unset, tuple[None, bytes, str]] = UNSET - if not isinstance(self.an_enum_value_with_only_null, Unset): - _temp_an_enum_value_with_only_null = self.an_enum_value_with_only_null - an_enum_value_with_only_null = ( - None, - json.dumps(_temp_an_enum_value_with_only_null).encode(), - "application/json", - ) - - an_allof_enum_with_overridden_default: Union[Unset, tuple[None, bytes, str]] = UNSET - if not isinstance(self.an_allof_enum_with_overridden_default, Unset): - an_allof_enum_with_overridden_default = ( - None, - str(self.an_allof_enum_with_overridden_default).encode(), - "text/plain", - ) - - an_optional_allof_enum: Union[Unset, tuple[None, bytes, str]] = UNSET - if not isinstance(self.an_optional_allof_enum, Unset): - an_optional_allof_enum = (None, str(self.an_optional_allof_enum).encode(), "text/plain") - - nested_list_of_enums: Union[Unset, tuple[None, bytes, str]] = UNSET - if not isinstance(self.nested_list_of_enums, Unset): - _temp_nested_list_of_enums = [] - for nested_list_of_enums_item_data in self.nested_list_of_enums: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item: str = nested_list_of_enums_item_item_data - nested_list_of_enums_item.append(nested_list_of_enums_item_item) - - _temp_nested_list_of_enums.append(nested_list_of_enums_item) - nested_list_of_enums = (None, json.dumps(_temp_nested_list_of_enums).encode(), "application/json") - - field_dict: dict[str, Any] = {} - for prop_name, prop in self.additional_properties.items(): - field_dict[prop_name] = (None, str(prop).encode(), "text/plain") - - field_dict.update({}) - if an_enum_value is not UNSET: - field_dict["an_enum_value"] = an_enum_value - if an_enum_value_with_null is not UNSET: - field_dict["an_enum_value_with_null"] = an_enum_value_with_null - if an_enum_value_with_only_null is not UNSET: - field_dict["an_enum_value_with_only_null"] = an_enum_value_with_only_null - if an_allof_enum_with_overridden_default is not UNSET: - field_dict["an_allof_enum_with_overridden_default"] = an_allof_enum_with_overridden_default - if an_optional_allof_enum is not UNSET: - field_dict["an_optional_allof_enum"] = an_optional_allof_enum - if nested_list_of_enums is not UNSET: - field_dict["nested_list_of_enums"] = nested_list_of_enums - - return field_dict - - @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() - an_enum_value = [] - _an_enum_value = d.pop("an_enum_value", UNSET) - for an_enum_value_item_data in _an_enum_value or []: - an_enum_value_item = check_an_enum(an_enum_value_item_data) - - an_enum_value.append(an_enum_value_item) - - an_enum_value_with_null = [] - _an_enum_value_with_null = d.pop("an_enum_value_with_null", UNSET) - for an_enum_value_with_null_item_data in _an_enum_value_with_null or []: - - def _parse_an_enum_value_with_null_item(data: object) -> Union[AnEnumWithNull, None]: - if data is None: - return data - try: - if not isinstance(data, str): - raise TypeError() - componentsschemas_an_enum_with_null_type_1 = check_an_enum_with_null(data) - - return componentsschemas_an_enum_with_null_type_1 - except: # noqa: E722 - pass - return cast(Union[AnEnumWithNull, None], data) - - an_enum_value_with_null_item = _parse_an_enum_value_with_null_item(an_enum_value_with_null_item_data) - - an_enum_value_with_null.append(an_enum_value_with_null_item) - - an_enum_value_with_only_null = cast(list[None], d.pop("an_enum_value_with_only_null", UNSET)) - - _an_allof_enum_with_overridden_default = d.pop("an_allof_enum_with_overridden_default", UNSET) - an_allof_enum_with_overridden_default: Union[Unset, AnAllOfEnum] - if isinstance(_an_allof_enum_with_overridden_default, Unset): - an_allof_enum_with_overridden_default = UNSET - else: - an_allof_enum_with_overridden_default = check_an_all_of_enum(_an_allof_enum_with_overridden_default) - - _an_optional_allof_enum = d.pop("an_optional_allof_enum", UNSET) - an_optional_allof_enum: Union[Unset, AnAllOfEnum] - if isinstance(_an_optional_allof_enum, Unset): - an_optional_allof_enum = UNSET - else: - an_optional_allof_enum = check_an_all_of_enum(_an_optional_allof_enum) - - nested_list_of_enums = [] - _nested_list_of_enums = d.pop("nested_list_of_enums", UNSET) - for nested_list_of_enums_item_data in _nested_list_of_enums or []: - nested_list_of_enums_item = [] - _nested_list_of_enums_item = nested_list_of_enums_item_data - for nested_list_of_enums_item_item_data in _nested_list_of_enums_item: - nested_list_of_enums_item_item = check_different_enum(nested_list_of_enums_item_item_data) - - nested_list_of_enums_item.append(nested_list_of_enums_item_item) - - nested_list_of_enums.append(nested_list_of_enums_item) - - post_user_list_body = cls( - an_enum_value=an_enum_value, - an_enum_value_with_null=an_enum_value_with_null, - an_enum_value_with_only_null=an_enum_value_with_only_null, - an_allof_enum_with_overridden_default=an_allof_enum_with_overridden_default, - an_optional_allof_enum=an_optional_allof_enum, - nested_list_of_enums=nested_list_of_enums, - ) - - post_user_list_body.additional_properties = d - return post_user_list_body - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/types.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/types.py deleted file mode 100644 index b9ed58b8a..000000000 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/types.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Contains some shared types for properties""" - -from collections.abc import MutableMapping -from http import HTTPStatus -from typing import BinaryIO, Generic, Literal, Optional, TypeVar - -from attrs import define - - -class Unset: - def __bool__(self) -> Literal[False]: - return False - - -UNSET: Unset = Unset() - -FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]] - - -@define -class File: - """Contains information for file uploads""" - - payload: BinaryIO - file_name: Optional[str] = None - mime_type: Optional[str] = None - - def to_tuple(self) -> FileJsonType: - """Return a tuple representation that httpx will accept for multipart/form-data""" - return self.file_name, self.payload, self.mime_type - - -T = TypeVar("T") - - -@define -class Response(Generic[T]): - """A response from an endpoint""" - - status_code: HTTPStatus - content: bytes - headers: MutableMapping[str, str] - parsed: Optional[T] - - -__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"] diff --git a/end_to_end_tests/metadata_snapshots/pdm.pyproject.toml b/end_to_end_tests/metadata_snapshots/pdm.pyproject.toml index c1f8a2a2b..44be8e61e 100644 --- a/end_to_end_tests/metadata_snapshots/pdm.pyproject.toml +++ b/end_to_end_tests/metadata_snapshots/pdm.pyproject.toml @@ -4,11 +4,10 @@ version = "0.1.0" description = "A client library for accessing Test 3.1 Features" authors = [] readme = "README.md" -requires-python = ">=3.9,<4.0" +requires-python = ">=3.11" dependencies = [ - "httpx>=0.20.0,<0.29.0", + "httpx2>=2.13.1,<3", "attrs>=22.2.0", - "python-dateutil>=2.8.0", ] [tool.pdm] diff --git a/end_to_end_tests/metadata_snapshots/poetry.pyproject.toml b/end_to_end_tests/metadata_snapshots/poetry.pyproject.toml index 2e8cd6c04..e92a71149 100644 --- a/end_to_end_tests/metadata_snapshots/poetry.pyproject.toml +++ b/end_to_end_tests/metadata_snapshots/poetry.pyproject.toml @@ -5,19 +5,17 @@ description = "A client library for accessing Test 3.1 Features" authors = [] readme = "README.md" packages = [ - {include = "test_3_1_features_client"}, + { include = "test_3_1_features_client" }, ] -include = ["CHANGELOG.md", "test_3_1_features_client/py.typed"] - +include = ["test_3_1_features_client/py.typed"] [tool.poetry.dependencies] -python = "^3.9" -httpx = ">=0.20.0,<0.29.0" +python = "^3.11" +httpx2 = ">=2.13.1,<3" attrs = ">=22.2.0" -python-dateutil = "^2.8.0" [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.ruff] diff --git a/end_to_end_tests/metadata_snapshots/setup.py b/end_to_end_tests/metadata_snapshots/setup.py index 6c7a58b97..066d707c3 100644 --- a/end_to_end_tests/metadata_snapshots/setup.py +++ b/end_to_end_tests/metadata_snapshots/setup.py @@ -12,7 +12,7 @@ long_description=long_description, long_description_content_type="text/markdown", packages=find_packages(), - python_requires=">=3.9, <4", - install_requires=["httpx >= 0.20.0, < 0.29.0", "attrs >= 22.2.0", "python-dateutil >= 2.8.0, < 3"], + python_requires=">=3.11, <4", + install_requires=["httpx2 >= 2.13.1, < 3", "attrs >= 22.2.0"], package_data={"test_3_1_features_client": ["py.typed"]}, ) diff --git a/end_to_end_tests/metadata_snapshots/uv.pyproject.toml b/end_to_end_tests/metadata_snapshots/uv.pyproject.toml new file mode 100644 index 000000000..f784093a5 --- /dev/null +++ b/end_to_end_tests/metadata_snapshots/uv.pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "test-3-1-features-client" +version = "0.1.0" +description = "A client library for accessing Test 3.1 Features" +authors = [] +requires-python = ">=3.11" +readme = "README.md" +dependencies = [ + "httpx2>=2.13.1,<3", + "attrs>=22.2.0", +] + +[tool.uv.build-backend] +module-name = "test_3_1_features_client" +module-root = "" + +[build-system] +requires = ["uv_build>=0.13.0,<0.14.0"] +build-backend = "uv_build" + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = ["F", "I", "UP"] diff --git a/end_to_end_tests/regen_golden_record.py b/end_to_end_tests/regen_golden_record.py index 2471e1340..425775530 100644 --- a/end_to_end_tests/regen_golden_record.py +++ b/end_to_end_tests/regen_golden_record.py @@ -1,8 +1,7 @@ -""" Regenerate golden-record """ +"""Regenerate golden-record""" + import filecmp -import os import shutil -import tempfile from pathlib import Path from typer.testing import CliRunner @@ -10,99 +9,116 @@ from openapi_python_client.cli import app -def regen_golden_record(): +def _regenerate( + *, + spec_file_name: str, + output_dir: str, + capture_golden_record: bool = False, + config_file_name: str = "config.yml", + extra_args: list[str] | None = None, +) -> None: + end_to_end_tests_base_path = Path(__file__).parent runner = CliRunner() - openapi_path = Path(__file__).parent / "baseline_openapi_3.0.json" - - gr_path = Path(__file__).parent / "golden-record" - output_path = Path.cwd() / "my-test-api-client" - config_path = Path(__file__).parent / "config.yml" + openapi_path = end_to_end_tests_base_path / spec_file_name - shutil.rmtree(gr_path, ignore_errors=True) + output_path = end_to_end_tests_base_path / "tmp" / output_dir shutil.rmtree(output_path, ignore_errors=True) - result = runner.invoke( - app, ["generate", f"--config={config_path}", f"--path={openapi_path}"] - ) + args = ["generate", f"--path={openapi_path}", f"--output-path={output_path}"] + if config_file_name: + config_path = end_to_end_tests_base_path / config_file_name + args.append(f"--config={config_path}") + if extra_args: + args.extend(extra_args) + print(f"Using {spec_file_name}{f' and {config_file_name}' if config_file_name else ''}") + + result = runner.invoke(app, args) if result.stdout: print(result.stdout) if result.exception: - raise result.exception - output_path.rename(gr_path) - + raise Exception(f"{result.exception} {result.stderr}") + if capture_golden_record: + gr_path = end_to_end_tests_base_path / "golden-records" / output_dir + shutil.rmtree(gr_path, ignore_errors=True) + output_path.rename(gr_path) -def regen_golden_record_3_1_features(): - runner = CliRunner() - openapi_path = Path(__file__).parent / "3.1_specific.openapi.yaml" - - gr_path = Path(__file__).parent / "test-3-1-golden-record" - output_path = Path.cwd() / "test-3-1-features-client" - shutil.rmtree(gr_path, ignore_errors=True) - shutil.rmtree(output_path, ignore_errors=True) +def regen_golden_record(): + _regenerate( + spec_file_name="baseline_openapi_3.0.json", + output_dir="my-test-api-client", + capture_golden_record=True, + ) - result = runner.invoke(app, ["generate", f"--path={openapi_path}"]) - if result.stdout: - print(result.stdout) - if result.exception: - raise result.exception - output_path.rename(gr_path) +def regen_golden_record_3_1_features(): + _regenerate( + spec_file_name="3.1_specific.openapi.yaml", + output_dir="test-3-1-features-client", + capture_golden_record=True, + ) def regen_literal_enums_golden_record(): - runner = CliRunner() - openapi_path = Path(__file__).parent / "openapi_3.1_enums.yaml" - - gr_path = Path(__file__).parent / "literal-enums-golden-record" - output_path = Path.cwd() / "my-enum-api-client" - config_path = Path(__file__).parent / "literal_enums.config.yml" - - shutil.rmtree(gr_path, ignore_errors=True) - shutil.rmtree(output_path, ignore_errors=True) + _regenerate( + spec_file_name="openapi_3.1_enums.yaml", + output_dir="my-enum-api-client", + capture_golden_record=True, + config_file_name="literal_enums.config.yml", + ) - result = runner.invoke(app, ["generate", f"--path={openapi_path}", f"--config={config_path}"]) - if result.stdout: - print(result.stdout) - if result.exception: - raise result.exception - output_path.rename(gr_path) +def regen_escapes_golden_record(): + _regenerate( + spec_file_name="escapes.openapi.yaml", + output_dir="escapes-client", + capture_golden_record=True, + ) def regen_metadata_snapshots(): - runner = CliRunner() - openapi_path = Path(__file__).parent / "3.1_specific.openapi.yaml" - output_path = Path.cwd() / "test-3-1-features-client" + output_path = Path(__file__).parent / "tmp" / "test-3-1-features-client" snapshots_dir = Path(__file__).parent / "metadata_snapshots" - for (meta, file, rename_to) in (("setup", "setup.py", "setup.py"), ("pdm", "pyproject.toml", "pdm.pyproject.toml"), ("poetry", "pyproject.toml", "poetry.pyproject.toml")): - shutil.rmtree(output_path, ignore_errors=True) - result = runner.invoke(app, ["generate", f"--path={openapi_path}", f"--meta={meta}"]) - - if result.stdout: - print(result.stdout) - if result.exception: - raise result.exception - + for meta, file, rename_to in ( + ("setup", "setup.py", "setup.py"), + ("pdm", "pyproject.toml", "pdm.pyproject.toml"), + ("poetry", "pyproject.toml", "poetry.pyproject.toml"), + ("uv", "pyproject.toml", "uv.pyproject.toml"), + ): + _regenerate( + spec_file_name="3.1_specific.openapi.yaml", + output_dir="test-3-1-features-client", + extra_args=[f"--meta={meta}"], + ) (output_path / file).rename(snapshots_dir / rename_to) shutil.rmtree(output_path, ignore_errors=True) +def regen_docstrings_on_attributes_golden_record(): + _regenerate( + spec_file_name="docstrings_on_attributes.yml", + output_dir="docstrings-on-attributes-golden-record", + capture_golden_record=True, + config_file_name="docstrings_on_attributes.config.yml", + ) + + def regen_custom_template_golden_record(): runner = CliRunner() openapi_path = Path(__file__).parent / "baseline_openapi_3.0.json" tpl_dir = Path(__file__).parent / "test_custom_templates" - gr_path = Path(__file__).parent / "golden-record" - tpl_gr_path = Path(__file__).parent / "custom-templates-golden-record" + gr_path = Path(__file__).parent / "golden-records" / "my-test-api-client" + tpl_gr_path = Path(__file__).parent / "golden-records" / "custom-templates" - output_path = Path.cwd() / "my-test-api-client" + output_path = Path(__file__).parent / "tmp" / "my-test-api-client" config_path = Path(__file__).parent / "config.yml" shutil.rmtree(tpl_gr_path, ignore_errors=True) + shutil.rmtree(output_path, ignore_errors=True) result = runner.invoke( app, @@ -111,6 +127,7 @@ def regen_custom_template_golden_record(): f"--config={config_path}", f"--path={openapi_path}", f"--custom-template-path={tpl_dir}", + f"--output-path={output_path}", ], ) @@ -143,5 +160,7 @@ def regen_custom_template_golden_record(): regen_golden_record() regen_golden_record_3_1_features() regen_metadata_snapshots() + regen_docstrings_on_attributes_golden_record() regen_custom_template_golden_record() regen_literal_enums_golden_record() + regen_escapes_golden_record() diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/__init__.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/__init__.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/prefix_items/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/post_prefix_items_body.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/post_prefix_items_body.py deleted file mode 100644 index a578b3091..000000000 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/post_prefix_items_body.py +++ /dev/null @@ -1,103 +0,0 @@ -from typing import Any, Literal, TypeVar, Union, cast - -from attrs import define as _attrs_define -from attrs import field as _attrs_field - -from ..types import UNSET, Unset - -T = TypeVar("T", bound="PostPrefixItemsBody") - - -@_attrs_define -class PostPrefixItemsBody: - """ - Attributes: - prefix_items_and_items (Union[Unset, list[Union[Literal['prefix'], float, str]]]): - prefix_items_only (Union[Unset, list[Union[float, str]]]): - """ - - prefix_items_and_items: Union[Unset, list[Union[Literal["prefix"], float, str]]] = UNSET - prefix_items_only: Union[Unset, list[Union[float, str]]] = UNSET - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) - - def to_dict(self) -> dict[str, Any]: - prefix_items_and_items: Union[Unset, list[Union[Literal["prefix"], float, str]]] = UNSET - if not isinstance(self.prefix_items_and_items, Unset): - prefix_items_and_items = [] - for prefix_items_and_items_item_data in self.prefix_items_and_items: - prefix_items_and_items_item: Union[Literal["prefix"], float, str] - prefix_items_and_items_item = prefix_items_and_items_item_data - prefix_items_and_items.append(prefix_items_and_items_item) - - prefix_items_only: Union[Unset, list[Union[float, str]]] = UNSET - if not isinstance(self.prefix_items_only, Unset): - prefix_items_only = [] - for prefix_items_only_item_data in self.prefix_items_only: - prefix_items_only_item: Union[float, str] - prefix_items_only_item = prefix_items_only_item_data - prefix_items_only.append(prefix_items_only_item) - - field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) - field_dict.update({}) - if prefix_items_and_items is not UNSET: - field_dict["prefixItemsAndItems"] = prefix_items_and_items - if prefix_items_only is not UNSET: - field_dict["prefixItemsOnly"] = prefix_items_only - - return field_dict - - @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() - prefix_items_and_items = [] - _prefix_items_and_items = d.pop("prefixItemsAndItems", UNSET) - for prefix_items_and_items_item_data in _prefix_items_and_items or []: - - def _parse_prefix_items_and_items_item(data: object) -> Union[Literal["prefix"], float, str]: - prefix_items_and_items_item_type_0 = cast(Literal["prefix"], data) - if prefix_items_and_items_item_type_0 != "prefix": - raise ValueError( - f"prefixItemsAndItems_item_type_0 must match const 'prefix', got '{prefix_items_and_items_item_type_0}'" - ) - return prefix_items_and_items_item_type_0 - return cast(Union[Literal["prefix"], float, str], data) - - prefix_items_and_items_item = _parse_prefix_items_and_items_item(prefix_items_and_items_item_data) - - prefix_items_and_items.append(prefix_items_and_items_item) - - prefix_items_only = [] - _prefix_items_only = d.pop("prefixItemsOnly", UNSET) - for prefix_items_only_item_data in _prefix_items_only or []: - - def _parse_prefix_items_only_item(data: object) -> Union[float, str]: - return cast(Union[float, str], data) - - prefix_items_only_item = _parse_prefix_items_only_item(prefix_items_only_item_data) - - prefix_items_only.append(prefix_items_only_item) - - post_prefix_items_body = cls( - prefix_items_and_items=prefix_items_and_items, - prefix_items_only=prefix_items_only, - ) - - post_prefix_items_body.additional_properties = d - return post_prefix_items_body - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/types.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/types.py deleted file mode 100644 index b9ed58b8a..000000000 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/types.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Contains some shared types for properties""" - -from collections.abc import MutableMapping -from http import HTTPStatus -from typing import BinaryIO, Generic, Literal, Optional, TypeVar - -from attrs import define - - -class Unset: - def __bool__(self) -> Literal[False]: - return False - - -UNSET: Unset = Unset() - -FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]] - - -@define -class File: - """Contains information for file uploads""" - - payload: BinaryIO - file_name: Optional[str] = None - mime_type: Optional[str] = None - - def to_tuple(self) -> FileJsonType: - """Return a tuple representation that httpx will accept for multipart/form-data""" - return self.file_name, self.payload, self.mime_type - - -T = TypeVar("T") - - -@define -class Response(Generic[T]): - """A response from an endpoint""" - - status_code: HTTPStatus - content: bytes - headers: MutableMapping[str, str] - parsed: Optional[T] - - -__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"] diff --git a/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja b/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja index dd64d942c..aa207c418 100644 --- a/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja +++ b/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja @@ -13,11 +13,11 @@ class {{ class_name(endpoint_collection.tag) }}Endpoints: def {{ python_identifier(endpoint.name) }}(cls) -> types.ModuleType: {% if endpoint.description %} """ - {{ endpoint.description }} + {{ endpoint.description | safe_for_docstring }} """ {% elif endpoint.summary %} """ - {{ endpoint.summary }} + {{ endpoint.summary | safe_for_docstring }} """ {% endif %} return {{ python_identifier(endpoint.name) }} diff --git a/end_to_end_tests/test_custom_templates/models_init.py.jinja b/end_to_end_tests/test_custom_templates/models_init.py.jinja new file mode 100644 index 000000000..8b0a55aee --- /dev/null +++ b/end_to_end_tests/test_custom_templates/models_init.py.jinja @@ -0,0 +1,33 @@ + +# Testing that we can access model-related information via Jinja variables. + +# To avoid having to update this file in the golden record every time the test specs are changed, +# we won't include all the classes in this output - we'll just look for one of them. + +# Using "alls" +{% for name in alls %} +{% if name == "AModel" %} +# {{ name }} +{% endif %} +{% endfor %} + +# Using "imports" +{% for import in imports %} +{% if import.endswith("import AModel") %} +# {{ import }} +{% endif %} +{% endfor %} + +# Using "openapi.models" +{% for model in openapi.models %} +{% if model.class_info.name == "AModel" %} +# {{ model.class_info.name }} ({{ model.class_info.module_name }}) +{% endif %} +{% endfor %} + +# Using "openapi.enums" +{% for enum in openapi.enums %} +{% if enum.class_info.name == "AnEnum" %} +# {{ enum.class_info.name }} ({{ enum.class_info.module_name }}) +{% endif %} +{% endfor %} diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index 124b801d2..486382e95 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -1,21 +1,26 @@ import shutil +import subprocess +import sys from filecmp import cmpfiles, dircmp from pathlib import Path -from typing import Optional import pytest -from click.testing import Result -from typer.testing import CliRunner +from typer.testing import CliRunner, Result +from end_to_end_tests.generated_client import ( + _run_command, + generate_client, +) from openapi_python_client.cli import app def _compare_directories( + *, record: Path, test_subject: Path, - expected_differences: Optional[dict[Path, str]] = None, - expected_missing: Optional[set[str]] = None, - ignore: list[str] = None, + expected_differences: dict[Path, str] | None = None, + expected_missing: set[str] | None = None, + ignore: list[str] | None = None, depth=0, ): """ @@ -38,9 +43,7 @@ def _compare_directories( ) expected_differences = expected_differences or {} - _, mismatches, errors = cmpfiles( - record, test_subject, dc.common_files, shallow=False - ) + _, mismatches, _errors = cmpfiles(record, test_subject, dc.common_files, shallow=False) mismatches = set(mismatches) for file_name in mismatches: @@ -53,81 +56,56 @@ def _compare_directories( expected_content = (record / file_name).read_text() generated_content = (test_subject / file_name).read_text() - assert ( - generated_content == expected_content - ), f"Unexpected output in {mismatch_file_path}" + assert generated_content == expected_content, f"Unexpected output in {mismatch_file_path}" for sub_path in dc.common_dirs: _compare_directories( - record / sub_path, - test_subject / sub_path, + record=record / sub_path, + test_subject=test_subject / sub_path, expected_differences=expected_differences, ignore=ignore, depth=depth + 1, ) if depth == 0 and len(expected_differences.keys()) > 0: - failure = "\n".join( - [ - f"Expected {path} to be different but it was not" - for path in expected_differences.keys() - ] - ) + failure = "\n".join([f"Expected {path} to be different but it was not" for path in expected_differences.keys()]) pytest.fail(failure, pytrace=False) def run_e2e_test( openapi_document: str, extra_args: list[str], - expected_differences: Optional[dict[Path, str]] = None, - golden_record_path: str = "golden-record", + expected_differences: dict[Path, str] | None = None, output_path: str = "my-test-api-client", - expected_missing: Optional[set[str]] = None, + expected_missing: set[str] | None = None, + *, + golden_record_path: str | None = None, ) -> Result: - output_path = Path.cwd() / output_path - shutil.rmtree(output_path, ignore_errors=True) - result = generate(extra_args, openapi_document) - gr_path = Path(__file__).parent / golden_record_path - - expected_differences = expected_differences or {} - # Use absolute paths for expected differences for easier comparisons - expected_differences = { - output_path.joinpath(key): value for key, value in expected_differences.items() - } - _compare_directories( - gr_path, output_path, expected_differences=expected_differences, expected_missing=expected_missing - ) - - import mypy.api - - out, err, status = mypy.api.run([str(output_path), "--strict"]) - assert status == 0, f"Type checking client failed: {out}" - - shutil.rmtree(output_path) - return result - + with generate_client(openapi_document=openapi_document, extra_args=extra_args, output_path=output_path) as g: + if golden_record_path: + gr_path = Path(__file__).parent / "golden-records" / golden_record_path + else: + gr_path = Path(__file__).parent / "golden-records" / output_path -def generate(extra_args: Optional[list[str]], openapi_document: str) -> Result: - """Generate a client from an OpenAPI document and return the path to the generated code""" - _run_command("generate", extra_args, openapi_document) + expected_differences = expected_differences or {} + # Use absolute paths for expected differences for easier comparisons + expected_differences = {g.output_path.joinpath(key): value for key, value in expected_differences.items()} + _compare_directories( + record=gr_path, + test_subject=g.output_path, + expected_differences=expected_differences, + expected_missing=expected_missing, + ) + result = subprocess.run( + [sys.executable, "-m", "mypy", str(g.output_path), "--strict"], + capture_output=True, + text=True, + check=True, + ) + assert result.returncode == 0, f"Type checking client failed: {result.stdout}" -def _run_command(command: str, extra_args: Optional[list[str]] = None, openapi_document: Optional[str] = None, url: Optional[str] = None, config_path: Optional[Path] = None) -> Result: - """Generate a client from an OpenAPI document and return the path to the generated code""" - runner = CliRunner() - if openapi_document is not None: - openapi_path = Path(__file__).parent / openapi_document - source_arg = f"--path={openapi_path}" - else: - source_arg = f"--url={url}" - config_path = config_path or (Path(__file__).parent / "config.yml") - args = [command, f"--config={config_path}", source_arg] - if extra_args: - args.extend(extra_args) - result = runner.invoke(app, args) - if result.exit_code != 0: - raise Exception(result.stdout) - return result + return g.generator_result def test_baseline_end_to_end_3_0(): @@ -143,19 +121,21 @@ def test_3_1_specific_features(): "3.1_specific.openapi.yaml", [], {}, - "test-3-1-golden-record", "test-3-1-features-client", ) def test_literal_enums_end_to_end(): config_path = Path(__file__).parent / "literal_enums.config.yml" + run_e2e_test("openapi_3.1_enums.yaml", [f"--config={config_path}"], {}, "my-enum-api-client") + + +def test_escapes_end_to_end(): run_e2e_test( - "openapi_3.1_enums.yaml", - [f"--config={config_path}"], + "escapes.openapi.yaml", + [], {}, - "literal-enums-golden-record", - "my-enum-api-client" + "escapes-client", ) @@ -165,51 +145,56 @@ def test_literal_enums_end_to_end(): ("setup", "setup.py", "setup.py"), ("pdm", "pyproject.toml", "pdm.pyproject.toml"), ("poetry", "pyproject.toml", "poetry.pyproject.toml"), - ) + ("uv", "pyproject.toml", "uv.pyproject.toml"), + ), ) -def test_meta(meta: str, generated_file: Optional[str], expected_file: Optional[str]): - output_path = Path.cwd() / "test-3-1-features-client" - shutil.rmtree(output_path, ignore_errors=True) - generate([f"--meta={meta}"], "3.1_specific.openapi.yaml") - - if generated_file and expected_file: - assert (output_path / generated_file).exists() - assert ( - (output_path / generated_file).read_text() == - (Path(__file__).parent / "metadata_snapshots" / expected_file).read_text() - ) - - shutil.rmtree(output_path) +def test_meta(meta: str, generated_file: str | None, expected_file: str | None): + with generate_client( + openapi_document="3.1_specific.openapi.yaml", + extra_args=[f"--meta={meta}"], + output_path="test-3-1-features-client", + ) as g: + if generated_file and expected_file: + assert (g.output_path / generated_file).exists() + assert (g.output_path / generated_file).read_text() == ( + Path(__file__).parent / "metadata_snapshots" / expected_file + ).read_text() def test_none_meta(): run_e2e_test( "3.1_specific.openapi.yaml", ["--meta=none"], - golden_record_path="test-3-1-golden-record/test_3_1_features_client", + golden_record_path="test-3-1-features-client/test_3_1_features_client", output_path="test_3_1_features_client", expected_missing={"py.typed"}, ) +def test_docstrings_on_attributes(): + config_path = Path(__file__).parent / "docstrings_on_attributes.config.yml" + run_e2e_test( + "docstrings_on_attributes.yml", + [f"--config={config_path}"], + {}, + "docstrings-on-attributes-golden-record", + ) + + def test_custom_templates(): - expected_differences = ( - {} - ) # key: path relative to generated directory, value: expected generated content + expected_differences = {} # key: path relative to generated directory, value: expected generated content api_dir = Path("my_test_api_client").joinpath("api") - golden_tpls_root_dir = Path(__file__).parent.joinpath( - "custom-templates-golden-record" - ) + models_dir = Path("my_test_api_client").joinpath("models") + golden_tpls_root_dir = Path(__file__).parent / "golden-records" / "custom-templates" expected_difference_paths = [ Path("README.md"), api_dir.joinpath("__init__.py"), + models_dir.joinpath("__init__.py"), ] for expected_difference_path in expected_difference_paths: - expected_differences[expected_difference_path] = ( - golden_tpls_root_dir / expected_difference_path - ).read_text() + expected_differences[expected_difference_path] = (golden_tpls_root_dir / expected_difference_path).read_text() # Each API module (defined by tag) has a custom __init__.py in it now. for endpoint_mod in golden_tpls_root_dir.joinpath(api_dir).iterdir(): @@ -230,7 +215,7 @@ def test_bad_url(): runner = CliRunner() result = runner.invoke(app, ["generate", "--url=not_a_url"]) assert result.exit_code == 1 - assert "Could not get OpenAPI document from provided URL" in result.stdout + assert "Could not get OpenAPI document from provided URL" in result.stderr ERROR_DOCUMENTS = [path for path in Path(__file__).parent.joinpath("documents_with_errors").iterdir() if path.is_file()] @@ -238,73 +223,66 @@ def test_bad_url(): @pytest.mark.parametrize("document", ERROR_DOCUMENTS, ids=[path.stem for path in ERROR_DOCUMENTS]) def test_documents_with_errors(snapshot, document): - runner = CliRunner() - output_path = Path.cwd() / "test-documents-with-errors" - shutil.rmtree(output_path, ignore_errors=True) - result = runner.invoke(app, ["generate", f"--path={document}", "--fail-on-warning", f"--output-path={output_path}"]) - assert result.exit_code == 1 - assert result.stdout.replace(str(output_path), "/test-documents-with-errors") == snapshot - shutil.rmtree(output_path, ignore_errors=True) + with generate_client( + openapi_document=document, + extra_args=["--fail-on-warning"], + output_path="test-documents-with-errors", + raise_on_error=False, + ) as g: + result = g.generator_result + assert result.exit_code == 1 + output = (result.stdout + result.stderr).replace(str(g.output_path), "/test-documents-with-errors") + assert output == snapshot def test_custom_post_hooks(): - shutil.rmtree(Path.cwd() / "my-test-api-client", ignore_errors=True) - runner = CliRunner() - openapi_document = Path(__file__).parent / "baseline_openapi_3.0.json" config_path = Path(__file__).parent / "custom_post_hooks.config.yml" - result = runner.invoke(app, ["generate", f"--path={openapi_document}", f"--config={config_path}"]) - assert result.exit_code == 1 - assert "this should fail" in result.stdout - shutil.rmtree(Path.cwd() / "my-test-api-client", ignore_errors=True) + with generate_client( + openapi_document="baseline_openapi_3.0.json", + extra_args=[f"--config={config_path}"], + raise_on_error=False, + ) as g: + assert g.generator_result.exit_code == 1 + assert "this should fail" in g.generator_result.stderr def test_generate_dir_already_exists(): project_dir = Path.cwd() / "my-test-api-client" if not project_dir.exists(): project_dir.mkdir() - runner = CliRunner() - openapi_document = Path(__file__).parent / "baseline_openapi_3.0.json" - result = runner.invoke(app, ["generate", f"--path={openapi_document}"]) - assert result.exit_code == 1 - assert "Directory already exists" in result.stdout - shutil.rmtree(Path.cwd() / "my-test-api-client", ignore_errors=True) - - -@pytest.mark.parametrize( - ("file_name", "content", "expected_error"), - ( - ("invalid_openapi.yaml", "not a valid openapi document", "Failed to parse OpenAPI document"), - ("invalid_json.json", "Invalid JSON", "Invalid JSON"), - ("invalid_yaml.yaml", "{", "Invalid YAML"), - ), - ids=("invalid_openapi", "invalid_json", "invalid_yaml") -) -def test_invalid_openapi_document(file_name, content, expected_error): - runner = CliRunner() - openapi_document = Path.cwd() / file_name - openapi_document.write_text(content) - result = runner.invoke(app, ["generate", f"--path={openapi_document}"]) - assert result.exit_code == 1 - assert expected_error in result.stdout - openapi_document.unlink() + try: + runner = CliRunner() + openapi_document = Path(__file__).parent / "baseline_openapi_3.0.json" + result = runner.invoke(app, ["generate", f"--path={openapi_document}"]) + assert result.exit_code == 1 + assert "Directory already exists" in result.stderr + finally: + shutil.rmtree(Path.cwd() / "my-test-api-client", ignore_errors=True) def test_update_integration_tests(): - url = "https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.json" + url = "https://raw.githubusercontent.com/openapi-generators/openapi-test-server/refs/tags/v0.2.1/openapi.yaml" source_path = Path(__file__).parent.parent / "integration-tests" temp_dir = Path.cwd() / "test_update_integration_tests" shutil.rmtree(temp_dir, ignore_errors=True) - shutil.copytree(source_path, temp_dir) - config_path = source_path / "config.yaml" - _run_command( - "generate", - extra_args=["--overwrite", "--meta=pdm", f"--output-path={temp_dir}"], - url=url, - config_path=config_path - ) - _compare_directories(source_path, temp_dir, ignore=["pyproject.toml"]) - import mypy.api - out, err, status = mypy.api.run([str(temp_dir), "--strict"]) - assert status == 0, f"Type checking client failed: {out}" - shutil.rmtree(temp_dir) + try: + shutil.copytree(source_path, temp_dir) + config_path = source_path / "config.yaml" + _run_command( + "generate", + extra_args=["--overwrite", "--meta=pdm", f"--output-path={temp_dir}"], + url=url, + config_path=config_path, + ) + _compare_directories(record=source_path, test_subject=temp_dir, ignore=["pyproject.toml"]) + result = subprocess.run( + [sys.executable, "-m", "mypy", str(temp_dir), "--strict"], + capture_output=True, + text=True, + check=True, + ) + assert result.returncode == 0, f"Type checking client failed: {result.stdout=} {result.stderr=}" + + finally: + shutil.rmtree(temp_dir) diff --git a/fuzz/test_codegen.py b/fuzz/test_codegen.py new file mode 100644 index 000000000..97b0becc2 --- /dev/null +++ b/fuzz/test_codegen.py @@ -0,0 +1,349 @@ +import ast +import io +import json +import os +import subprocess +import tomllib +import warnings +from contextlib import redirect_stdout +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Any + +from hypothesis import given, settings +from hypothesis import strategies as st + +from openapi_python_client import Config, ErrorLevel, MetaType, generate +from openapi_python_client.config import ConfigFile +from openapi_python_client.schema import OpenAPI + +FUZZ_EXAMPLES = int(os.environ.get("OPENAPI_PYTHON_CLIENT_FUZZ_EXAMPLES", "100")) + +# Canary payload: if any untrusted string breaks out of its literal/docstring in generated code, +# this appears as a real AST Name/Call node instead of text inside a string constant. +CANARY = "PWNED_CANARY" +CALL_CANARY = "EXECUTED_CANARY" + +DANGEROUS_STRINGS = ( + "", + "'", + '"', + "\\", + "\n", + "\r\n", + "\t", + "\x00", + '"""', + "'''", + "{value!r}", + "${value}", + "{{ value }}", + "line one\\\nline two", + "\\U0000000", + "\\N{NOT A REAL NAME}", + "# type: ignore\n" + CANARY + "()", + "\n# comment with trailing slash\\", + "from os import system", + "\N{LINE SEPARATOR}", + "\N{PILE OF POO}", + f'"{CANARY}("', + f'"{CANARY}() + "', + f"'''{CANARY}()'''", + f'"""{CANARY}()"""', + f'\\"{CANARY}()', + f"\n{CANARY}()\n", + f'"; print("{CALL_CANARY}"); _fuzz = "', + f'"""\nprint("{CALL_CANARY}")\n"""', +) +UTF8_CHARACTERS = st.characters(blacklist_categories=["Cs"]) +UNTRUSTED_TEXT = st.one_of( + st.sampled_from(DANGEROUS_STRINGS), + st.text(UTF8_CHARACTERS, max_size=60), +) +NONEMPTY_UNTRUSTED_TEXT = st.one_of( + st.sampled_from(tuple(value for value in DANGEROUS_STRINGS if value)), + st.text(UTF8_CHARACTERS, min_size=1, max_size=40), +) +COMPONENT_NAME = st.one_of( + st.sampled_from(tuple(value for value in DANGEROUS_STRINGS if value and "/" not in value and "~" not in value)), + st.text(UTF8_CHARACTERS, min_size=1, max_size=30).filter(lambda value: "/" not in value and "~" not in value), +) + + +@st.composite +def schema_strategy(draw: st.DrawFn) -> dict[str, Any]: + description = draw(UNTRUSTED_TEXT) + kind = draw( + st.sampled_from( + ("string", "integer", "number", "boolean", "array", "object", "reference", "union", "const", "free_form") + ) + ) + + value: Any + schema: dict[str, Any] = {"description": description} + if kind == "string": + value = draw(UNTRUSTED_TEXT) + schema.update({"type": "string", "default": value, "example": value}) + elif kind == "integer": + value = draw(st.integers(min_value=-(2**31), max_value=2**31 - 1)) + schema.update({"type": "integer", "default": value, "example": value}) + elif kind == "number": + value = draw(st.floats(allow_nan=False, allow_infinity=False, width=32)) + schema.update({"type": "number", "default": value, "example": value}) + elif kind == "boolean": + value = draw(st.booleans()) + schema.update({"type": "boolean", "default": value, "example": value}) + elif kind == "array": + schema.update( + { + "type": "array", + "items": {"type": "string", "description": draw(UNTRUSTED_TEXT)}, + "example": [draw(UNTRUSTED_TEXT)], + } + ) + elif kind == "object": + schema.update( + { + "title": draw(UNTRUSTED_TEXT), + "type": "object", + "properties": {"nested": {"type": "string", "description": draw(UNTRUSTED_TEXT)}}, + "additionalProperties": {"type": "string", "description": draw(UNTRUSTED_TEXT)}, + "example": {"nested": draw(UNTRUSTED_TEXT)}, + } + ) + elif kind == "union": + schema = { + "oneOf": [ + {"type": "string", "description": description}, + {"type": "integer", "description": draw(UNTRUSTED_TEXT)}, + ], + "description": draw(UNTRUSTED_TEXT), + } + elif kind == "const": + schema = {"const": draw(UNTRUSTED_TEXT), "description": description} + elif kind == "free_form": + schema = {"type": "object", "additionalProperties": True, "description": description} + else: + schema = {"$ref": "#/components/schemas/Choice"} + return schema + + +@st.composite +def openapi_documents(draw: st.DrawFn) -> dict[str, Any]: + component_name = draw(COMPONENT_NAME) + property_name = draw(NONEMPTY_UNTRUSTED_TEXT) + query_name = draw(NONEMPTY_UNTRUSTED_TEXT) + header_name = draw(NONEMPTY_UNTRUSTED_TEXT) + cookie_name = draw(NONEMPTY_UNTRUSTED_TEXT) + path_segment = draw(NONEMPTY_UNTRUSTED_TEXT) + security_name = draw(NONEMPTY_UNTRUSTED_TEXT) + property_schema = draw(schema_strategy()) + parameter_schema = draw(schema_strategy()) + enum_values = draw(st.lists(UNTRUSTED_TEXT, min_size=1, max_size=4, unique=True)) + int_enum_values = draw(st.lists(st.integers(min_value=-100, max_value=100), min_size=1, max_size=4, unique=True)) + + return { + "openapi": draw(st.sampled_from(("3.0.0", "3.0.3", "3.1.0"))), + "info": { + "title": draw(UNTRUSTED_TEXT), + "description": draw(UNTRUSTED_TEXT), + "version": draw(UNTRUSTED_TEXT), + "termsOfService": draw(UNTRUSTED_TEXT), + "contact": { + "name": draw(UNTRUSTED_TEXT), + "url": draw(UNTRUSTED_TEXT), + "email": draw(UNTRUSTED_TEXT), + }, + "license": {"name": draw(NONEMPTY_UNTRUSTED_TEXT), "url": draw(UNTRUSTED_TEXT)}, + }, + "servers": [ + { + "url": draw(UNTRUSTED_TEXT), + "description": draw(UNTRUSTED_TEXT), + "variables": { + "fuzz": { + "default": draw(UNTRUSTED_TEXT), + "enum": [draw(UNTRUSTED_TEXT)], + "description": draw(UNTRUSTED_TEXT), + } + }, + } + ], + "tags": [{"name": draw(NONEMPTY_UNTRUSTED_TEXT), "description": draw(UNTRUSTED_TEXT)}], + "externalDocs": {"url": draw(UNTRUSTED_TEXT), "description": draw(UNTRUSTED_TEXT)}, + "security": [{security_name: []}], + "paths": { + f"/widgets/{{widget_id}}/{path_segment}": { + "summary": draw(UNTRUSTED_TEXT), + "description": draw(UNTRUSTED_TEXT), + "post": { + "operationId": draw(NONEMPTY_UNTRUSTED_TEXT), + "tags": [draw(UNTRUSTED_TEXT)], + "summary": draw(UNTRUSTED_TEXT), + "description": draw(UNTRUSTED_TEXT), + "parameters": [ + { + "name": "widget_id", + "in": "path", + "required": True, + "description": draw(UNTRUSTED_TEXT), + "schema": {"type": "string"}, + }, + { + "name": query_name, + "in": "query", + "description": draw(UNTRUSTED_TEXT), + "schema": parameter_schema, + }, + { + "name": header_name, + "in": "header", + "description": draw(UNTRUSTED_TEXT), + "schema": {"type": "string", "default": draw(UNTRUSTED_TEXT)}, + }, + { + "name": cookie_name, + "in": "cookie", + "description": draw(UNTRUSTED_TEXT), + "schema": {"type": "string"}, + }, + ], + "requestBody": { + "required": True, + "description": draw(UNTRUSTED_TEXT), + "content": { + "application/json": { + "schema": {"$ref": f"#/components/schemas/{component_name}"}, + } + }, + }, + "responses": { + "200": { + "description": draw(UNTRUSTED_TEXT), + "headers": { + "X-Fuzz": { + "description": draw(UNTRUSTED_TEXT), + "schema": {"type": "string"}, + } + }, + "content": { + "application/json": { + "schema": {"$ref": f"#/components/schemas/{component_name}"}, + } + }, + } + }, + }, + } + }, + "components": { + "securitySchemes": { + security_name: { + "type": "apiKey", + "name": draw(NONEMPTY_UNTRUSTED_TEXT), + "in": "header", + "description": draw(UNTRUSTED_TEXT), + } + }, + "schemas": { + component_name: { + "title": draw(UNTRUSTED_TEXT), + "type": "object", + "description": draw(UNTRUSTED_TEXT), + "externalDocs": {"url": draw(UNTRUSTED_TEXT), "description": draw(UNTRUSTED_TEXT)}, + "xml": { + "name": draw(UNTRUSTED_TEXT), + "namespace": draw(UNTRUSTED_TEXT), + "prefix": draw(UNTRUSTED_TEXT), + }, + "required": [property_name], + "properties": {property_name: property_schema}, + }, + "Choice": { + "type": "string", + "description": draw(UNTRUSTED_TEXT), + "enum": enum_values, + }, + "IntChoice": { + "type": "integer", + "description": draw(UNTRUSTED_TEXT), + "enum": int_enum_values, + }, + }, + }, + } + + +@settings(max_examples=FUZZ_EXAMPLES, deadline=None) +@given(document=openapi_documents(), meta_type=st.sampled_from(list(MetaType))) +def test_valid_openapi_never_generates_invalid_python(document: dict[str, Any], meta_type: MetaType) -> None: + # The generator's own schema model is the source of truth for "valid": anything it accepts + # must produce syntactically valid Python. + OpenAPI.model_validate(document) + + with TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + document_path = root / "openapi.json" + output_path = root / "generated" + document_path.write_text(json.dumps(document, ensure_ascii=False), encoding="utf-8") + + config = Config.from_sources( + config_file=ConfigFile( + project_name_override="fuzz-client", + package_name_override="fuzz_client", + post_hooks=[], + ), + meta_type=meta_type, + document_source=document_path, + file_encoding="utf-8", + overwrite=False, + output_path=output_path, + ) + with redirect_stdout(io.StringIO()): + errors = generate(config=config) + + fatal_errors = [error for error in errors if error.level == ErrorLevel.ERROR] + assert not fatal_errors + + python_files = sorted(output_path.rglob("*.py")) + assert python_files + for python_file in python_files: + source = python_file.read_text(encoding="utf-8") + tree = ast.parse(source, filename=str(python_file)) + with warnings.catch_warnings(): + warnings.simplefilter("error") + compile(source, str(python_file), "exec") + _assert_no_code_injection(tree, python_file) + + for pyproject_file in output_path.rglob("pyproject.toml"): + with pyproject_file.open("rb") as pyproject: + tomllib.load(pyproject) + + # Static analysis catches escapes which produce valid syntax but reference names that + # don't exist (e.g. an injected function call) — even ones the canary doesn't cover. + ruff = subprocess.run( + ["ruff", "check", "--select", "F821", "--no-cache", str(output_path)], + capture_output=True, + text=True, + check=False, + ) + assert ruff.returncode == 0, f"Generated code references undefined names:\n{ruff.stdout}" + + +def _assert_no_code_injection(tree: ast.AST, python_file: Path) -> None: + """Fail if the canary escaped a string literal/docstring and became executable code.""" + for node in ast.walk(tree): + if isinstance(node, ast.Name) and CANARY in node.id: + raise AssertionError(f"Canary escaped into a name in {python_file}: {node.id}") + if isinstance(node, ast.Attribute) and CANARY in node.attr: + raise AssertionError(f"Canary escaped into an attribute in {python_file}: {node.attr}") + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and CANARY in node.name: + raise AssertionError(f"Canary escaped into a definition in {python_file}: {node.name}") + if isinstance(node, ast.ImportFrom) and node.module and CANARY in node.module: + raise AssertionError(f"Canary escaped into an import in {python_file}: {node.module}") + if isinstance(node, ast.alias) and CANARY in node.name: + raise AssertionError(f"Canary escaped into an import in {python_file}: {node.name}") + if isinstance(node, ast.Call) and any( + isinstance(child, ast.Constant) and child.value == CALL_CANARY for child in ast.walk(node) + ): + raise AssertionError(f"Built-in call canary became executable code in {python_file}") diff --git a/integration-tests/README.md b/integration-tests/README.md index f92972290..edd7d54fd 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -47,7 +47,7 @@ By default, when you're calling an HTTPS API it will attempt to verify that SSL ```python client = AuthenticatedClient( - base_url="https://internal_api.example.com", + base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl="/path/to/certificate_bundle.pem", ) @@ -56,11 +56,7 @@ client = AuthenticatedClient( You can also disable certificate validation altogether, but beware that **this is a security risk**. ```python -client = AuthenticatedClient( - base_url="https://internal_api.example.com", - token="SuperSecretToken", - verify_ssl=False -) +client = AuthenticatedClient(base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl=False) ``` Things to know: @@ -76,36 +72,39 @@ Things to know: ## Advanced customizations -There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx.Client` or `httpx.AsyncClient` (depending on your use-case): +There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx2.Client` or `httpx2.AsyncClient` (depending on your use-case): ```python from integration_tests import Client + def log_request(request): print(f"Request event hook: {request.method} {request.url} - Waiting for response") + def log_response(response): request = response.request print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") + client = Client( base_url="https://api.example.com", httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}}, ) -# Or get the underlying httpx client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() +# Or get the underlying httpx2 client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() ``` -You can even set the httpx client directly, but beware that this will override any existing settings (e.g., base_url): +You can even set the httpx2 client directly, but beware that this will override any existing settings (e.g., base_url): ```python -import httpx +import httpx2 from integration_tests import Client client = Client( base_url="https://api.example.com", ) # Note that base_url needs to be re-set, as would any shared cookies, headers, etc. -client.set_httpx_client(httpx.Client(base_url="https://api.example.com", proxies="http://localhost:8030")) +client.set_httpx_client(httpx2.Client(base_url="https://api.example.com", proxy="http://localhost:8030")) ``` diff --git a/integration-tests/integration_tests/api/body/__init__.py b/integration-tests/integration_tests/api/body/__init__.py index e69de29bb..2d7c0b23d 100644 --- a/integration-tests/integration_tests/api/body/__init__.py +++ b/integration-tests/integration_tests/api/body/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/integration-tests/integration_tests/api/body/post_body_multipart.py b/integration-tests/integration_tests/api/body/post_body_multipart.py index 13c943ad7..803ff4b91 100644 --- a/integration-tests/integration_tests/api/body/post_body_multipart.py +++ b/integration-tests/integration_tests/api/body/post_body_multipart.py @@ -1,19 +1,19 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client from ...models.post_body_multipart_body import PostBodyMultipartBody from ...models.post_body_multipart_response_200 import PostBodyMultipartResponse200 from ...models.public_error import PublicError -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, - body: PostBodyMultipartBody, + body: PostBodyMultipartBody | Unset = UNSET, ) -> dict[str, Any]: headers: dict[str, Any] = {} @@ -22,25 +22,26 @@ def _get_kwargs( "url": "/body/multipart", } - _body = body.to_multipart() - - _kwargs["files"] = _body + if not isinstance(body, Unset): + _kwargs["files"] = body.to_multipart() _kwargs["headers"] = headers return _kwargs def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Union[PostBodyMultipartResponse200, PublicError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> PostBodyMultipartResponse200 | PublicError | None: if response.status_code == 200: response_200 = PostBodyMultipartResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: response_400 = PublicError.from_dict(response.json()) return response_400 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -48,8 +49,8 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Union[PostBodyMultipartResponse200, PublicError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Response[PostBodyMultipartResponse200 | PublicError]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -60,19 +61,19 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], - body: PostBodyMultipartBody, -) -> Response[Union[PostBodyMultipartResponse200, PublicError]]: + client: AuthenticatedClient | Client, + body: PostBodyMultipartBody | Unset = UNSET, +) -> Response[PostBodyMultipartResponse200 | PublicError]: """ Args: - body (PostBodyMultipartBody): + body (PostBodyMultipartBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[PostBodyMultipartResponse200, PublicError]] + Response[PostBodyMultipartResponse200 | PublicError] """ kwargs = _get_kwargs( @@ -88,19 +89,19 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], - body: PostBodyMultipartBody, -) -> Optional[Union[PostBodyMultipartResponse200, PublicError]]: + client: AuthenticatedClient | Client, + body: PostBodyMultipartBody | Unset = UNSET, +) -> PostBodyMultipartResponse200 | PublicError | None: """ Args: - body (PostBodyMultipartBody): + body (PostBodyMultipartBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[PostBodyMultipartResponse200, PublicError] + PostBodyMultipartResponse200 | PublicError """ return sync_detailed( @@ -111,19 +112,19 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], - body: PostBodyMultipartBody, -) -> Response[Union[PostBodyMultipartResponse200, PublicError]]: + client: AuthenticatedClient | Client, + body: PostBodyMultipartBody | Unset = UNSET, +) -> Response[PostBodyMultipartResponse200 | PublicError]: """ Args: - body (PostBodyMultipartBody): + body (PostBodyMultipartBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[PostBodyMultipartResponse200, PublicError]] + Response[PostBodyMultipartResponse200 | PublicError] """ kwargs = _get_kwargs( @@ -137,19 +138,19 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], - body: PostBodyMultipartBody, -) -> Optional[Union[PostBodyMultipartResponse200, PublicError]]: + client: AuthenticatedClient | Client, + body: PostBodyMultipartBody | Unset = UNSET, +) -> PostBodyMultipartResponse200 | PublicError | None: """ Args: - body (PostBodyMultipartBody): + body (PostBodyMultipartBody | Unset): Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[PostBodyMultipartResponse200, PublicError] + PostBodyMultipartResponse200 | PublicError """ return ( diff --git a/integration-tests/integration_tests/api/parameters/__init__.py b/integration-tests/integration_tests/api/parameters/__init__.py index e69de29bb..2d7c0b23d 100644 --- a/integration-tests/integration_tests/api/parameters/__init__.py +++ b/integration-tests/integration_tests/api/parameters/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/integration-tests/integration_tests/api/parameters/post_parameters_header.py b/integration-tests/integration_tests/api/parameters/post_parameters_header.py index 0981585cc..efda5aef5 100644 --- a/integration-tests/integration_tests/api/parameters/post_parameters_header.py +++ b/integration-tests/integration_tests/api/parameters/post_parameters_header.py @@ -1,7 +1,7 @@ from http import HTTPStatus -from typing import Any, Optional, Union +from typing import Any -import httpx +import httpx2 from ... import errors from ...client import AuthenticatedClient, Client @@ -36,16 +36,18 @@ def _get_kwargs( def _parse_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Optional[Union[PostParametersHeaderResponse200, PublicError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> PostParametersHeaderResponse200 | PublicError | None: if response.status_code == 200: response_200 = PostParametersHeaderResponse200.from_dict(response.json()) return response_200 + if response.status_code == 400: response_400 = PublicError.from_dict(response.json()) return response_400 + if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: @@ -53,8 +55,8 @@ def _parse_response( def _build_response( - *, client: Union[AuthenticatedClient, Client], response: httpx.Response -) -> Response[Union[PostParametersHeaderResponse200, PublicError]]: + *, client: AuthenticatedClient | Client, response: httpx2.Response +) -> Response[PostParametersHeaderResponse200 | PublicError]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -65,12 +67,12 @@ def _build_response( def sync_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, boolean_header: bool, string_header: str, number_header: float, integer_header: int, -) -> Response[Union[PostParametersHeaderResponse200, PublicError]]: +) -> Response[PostParametersHeaderResponse200 | PublicError]: """ Args: boolean_header (bool): @@ -80,10 +82,10 @@ def sync_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[PostParametersHeaderResponse200, PublicError]] + Response[PostParametersHeaderResponse200 | PublicError] """ kwargs = _get_kwargs( @@ -102,12 +104,12 @@ def sync_detailed( def sync( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, boolean_header: bool, string_header: str, number_header: float, integer_header: int, -) -> Optional[Union[PostParametersHeaderResponse200, PublicError]]: +) -> PostParametersHeaderResponse200 | PublicError | None: """ Args: boolean_header (bool): @@ -117,10 +119,10 @@ def sync( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[PostParametersHeaderResponse200, PublicError] + PostParametersHeaderResponse200 | PublicError """ return sync_detailed( @@ -134,12 +136,12 @@ def sync( async def asyncio_detailed( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, boolean_header: bool, string_header: str, number_header: float, integer_header: int, -) -> Response[Union[PostParametersHeaderResponse200, PublicError]]: +) -> Response[PostParametersHeaderResponse200 | PublicError]: """ Args: boolean_header (bool): @@ -149,10 +151,10 @@ async def asyncio_detailed( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Union[PostParametersHeaderResponse200, PublicError]] + Response[PostParametersHeaderResponse200 | PublicError] """ kwargs = _get_kwargs( @@ -169,12 +171,12 @@ async def asyncio_detailed( async def asyncio( *, - client: Union[AuthenticatedClient, Client], + client: AuthenticatedClient | Client, boolean_header: bool, string_header: str, number_header: float, integer_header: int, -) -> Optional[Union[PostParametersHeaderResponse200, PublicError]]: +) -> PostParametersHeaderResponse200 | PublicError | None: """ Args: boolean_header (bool): @@ -184,10 +186,10 @@ async def asyncio( Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: - Union[PostParametersHeaderResponse200, PublicError] + PostParametersHeaderResponse200 | PublicError """ return ( diff --git a/integration-tests/integration_tests/client.py b/integration-tests/integration_tests/client.py index e80446f10..136b83d06 100644 --- a/integration-tests/integration_tests/client.py +++ b/integration-tests/integration_tests/client.py @@ -1,7 +1,7 @@ import ssl -from typing import Any, Optional, Union +from typing import Any, Self -import httpx +import httpx2 from attrs import define, evolve, field @@ -9,7 +9,7 @@ class Client: """A class for keeping track of data related to the API - The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: ``base_url``: The base URL for the API, all requests are made to a relative path to this URL @@ -18,14 +18,14 @@ class Client: ``headers``: A dictionary of headers to be sent with every request ``timeout``: The maximum amount of a time a request can take. API functions will raise - httpx.TimeoutException if this is exceeded. + httpx2.TimeoutException if this is exceeded. ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, but can be set to False for testing purposes. ``follow_redirects``: Whether or not to follow redirects. Default value is False. - ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. Attributes: @@ -38,12 +38,12 @@ class Client: _base_url: str = field(alias="base_url") _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") - _client: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) def with_headers(self, headers: dict[str, str]) -> "Client": """Get a new client matching this one with additional headers""" @@ -61,26 +61,26 @@ def with_cookies(self, cookies: dict[str, str]) -> "Client": self._async_client.cookies.update(cookies) return evolve(self, cookies={**self._cookies, **cookies}) - def with_timeout(self, timeout: httpx.Timeout) -> "Client": - """Get a new client matching this one with a new timeout (in seconds)""" + def with_timeout(self, timeout: httpx2.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" if self._client is not None: self._client.timeout = timeout if self._async_client is not None: self._async_client.timeout = timeout return evolve(self, timeout=timeout) - def set_httpx_client(self, client: httpx.Client) -> "Client": - """Manually set the underlying httpx.Client + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._client = client return self - def get_httpx_client(self) -> httpx.Client: - """Get the underlying httpx.Client, constructing a new one if not previously set""" + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" if self._client is None: - self._client = httpx.Client( + self._client = httpx2.Client( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -91,27 +91,27 @@ def get_httpx_client(self) -> httpx.Client: ) return self._client - def __enter__(self) -> "Client": - """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" self.get_httpx_client().__enter__() return self def __exit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for internal httpx.Client (see httpx docs)""" + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" self.get_httpx_client().__exit__(*args, **kwargs) - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client": - """Manually the underlying httpx.AsyncClient + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._async_client = async_client return self - def get_async_httpx_client(self) -> httpx.AsyncClient: - """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" if self._async_client is None: - self._async_client = httpx.AsyncClient( + self._async_client = httpx2.AsyncClient( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -122,13 +122,13 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: ) return self._async_client - async def __aenter__(self) -> "Client": - """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" await self.get_async_httpx_client().__aenter__() return self async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) @@ -136,7 +136,7 @@ async def __aexit__(self, *args: Any, **kwargs: Any) -> None: class AuthenticatedClient: """A Client which has been authenticated for use on secured endpoints - The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: ``base_url``: The base URL for the API, all requests are made to a relative path to this URL @@ -145,14 +145,14 @@ class AuthenticatedClient: ``headers``: A dictionary of headers to be sent with every request ``timeout``: The maximum amount of a time a request can take. API functions will raise - httpx.TimeoutException if this is exceeded. + httpx2.TimeoutException if this is exceeded. ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, but can be set to False for testing purposes. ``follow_redirects``: Whether or not to follow redirects. Default value is False. - ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. Attributes: @@ -168,12 +168,12 @@ class AuthenticatedClient: _base_url: str = field(alias="base_url") _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") - _client: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) token: str prefix: str = "Bearer" @@ -195,27 +195,27 @@ def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": self._async_client.cookies.update(cookies) return evolve(self, cookies={**self._cookies, **cookies}) - def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": - """Get a new client matching this one with a new timeout (in seconds)""" + def with_timeout(self, timeout: httpx2.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" if self._client is not None: self._client.timeout = timeout if self._async_client is not None: self._async_client.timeout = timeout return evolve(self, timeout=timeout) - def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": - """Manually set the underlying httpx.Client + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._client = client return self - def get_httpx_client(self) -> httpx.Client: - """Get the underlying httpx.Client, constructing a new one if not previously set""" + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" if self._client is None: self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token - self._client = httpx.Client( + self._client = httpx2.Client( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -226,28 +226,28 @@ def get_httpx_client(self) -> httpx.Client: ) return self._client - def __enter__(self) -> "AuthenticatedClient": - """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" self.get_httpx_client().__enter__() return self def __exit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for internal httpx.Client (see httpx docs)""" + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" self.get_httpx_client().__exit__(*args, **kwargs) - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient": - """Manually the underlying httpx.AsyncClient + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._async_client = async_client return self - def get_async_httpx_client(self) -> httpx.AsyncClient: - """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" if self._async_client is None: self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token - self._async_client = httpx.AsyncClient( + self._async_client = httpx2.AsyncClient( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -258,11 +258,11 @@ def get_async_httpx_client(self) -> httpx.AsyncClient: ) return self._async_client - async def __aenter__(self) -> "AuthenticatedClient": - """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" await self.get_async_httpx_client().__aenter__() return self async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/integration-tests/integration_tests/models/__init__.py b/integration-tests/integration_tests/models/__init__.py index 275cf6faa..257cfe9fa 100644 --- a/integration-tests/integration_tests/models/__init__.py +++ b/integration-tests/integration_tests/models/__init__.py @@ -1,5 +1,7 @@ """Contains all the data models used in inputs/outputs""" +from .an_object import AnObject +from .file import File from .post_body_multipart_body import PostBodyMultipartBody from .post_body_multipart_response_200 import PostBodyMultipartResponse200 from .post_parameters_header_response_200 import PostParametersHeaderResponse200 @@ -7,6 +9,8 @@ from .public_error import PublicError __all__ = ( + "AnObject", + "File", "PostBodyMultipartBody", "PostBodyMultipartResponse200", "PostParametersHeaderResponse200", diff --git a/integration-tests/integration_tests/models/an_object.py b/integration-tests/integration_tests/models/an_object.py new file mode 100644 index 000000000..228568b8c --- /dev/null +++ b/integration-tests/integration_tests/models/an_object.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="AnObject") + + +@_attrs_define +class AnObject: + """ + Attributes: + an_int (int): + a_float (float): + """ + + an_int: int + a_float: float + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + an_int = self.an_int + + a_float = self.a_float + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "an_int": an_int, + "a_float": a_float, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + an_int = d.pop("an_int") + + a_float = d.pop("a_float") + + an_object = cls( + an_int=an_int, + a_float=a_float, + ) + + an_object.additional_properties = d + return an_object + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/integration-tests/integration_tests/models/file.py b/integration-tests/integration_tests/models/file.py new file mode 100644 index 000000000..2380e8737 --- /dev/null +++ b/integration-tests/integration_tests/models/file.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="File") + + +@_attrs_define +class File: + """ + Attributes: + data (str | Unset): Echo of content of the 'file' input parameter from the form. + name (str | Unset): The name of the file uploaded. + content_type (str | Unset): The content type of the file uploaded. + """ + + data: str | Unset = UNSET + name: str | Unset = UNSET + content_type: str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + data = self.data + + name = self.name + + content_type = self.content_type + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if data is not UNSET: + field_dict["data"] = data + if name is not UNSET: + field_dict["name"] = name + if content_type is not UNSET: + field_dict["content_type"] = content_type + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + data = d.pop("data", UNSET) + + name = d.pop("name", UNSET) + + content_type = d.pop("content_type", UNSET) + + file = cls( + data=data, + name=name, + content_type=content_type, + ) + + file.additional_properties = d + return file + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/integration-tests/integration_tests/models/post_body_multipart_body.py b/integration-tests/integration_tests/models/post_body_multipart_body.py index 13174200d..0882f881b 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_body.py +++ b/integration-tests/integration_tests/models/post_body_multipart_body.py @@ -1,10 +1,20 @@ +from __future__ import annotations + +import datetime +import json +from collections.abc import Mapping from io import BytesIO -from typing import Any, TypeVar, Union +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field -from ..types import UNSET, File, Unset +from .. import types +from ..types import File + +if TYPE_CHECKING: + from ..models.an_object import AnObject + T = TypeVar("T", bound="PostBodyMultipartBody") @@ -14,75 +24,111 @@ class PostBodyMultipartBody: """ Attributes: a_string (str): - file (File): For the sake of this test, include a file name and content type. The payload should also be valid - UTF-8. - description (Union[Unset, str]): + files (list[File]): + description (str): + objects (list[AnObject]): + times (list[datetime.datetime]): """ a_string: str - file: File - description: Union[Unset, str] = UNSET + files: list[File] + description: str + objects: list[AnObject] + times: list[datetime.datetime] additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: a_string = self.a_string - file = self.file.to_tuple() + files = [] + for files_item_data in self.files: + files_item = files_item_data.to_tuple() + + files.append(files_item) description = self.description + objects = [] + for objects_item_data in self.objects: + objects_item = objects_item_data.to_dict() + objects.append(objects_item) + + times = [] + for times_item_data in self.times: + times_item = times_item_data.isoformat() + times.append(times_item) + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( { "a_string": a_string, - "file": file, + "files": files, + "description": description, + "objects": objects, + "times": times, } ) - if description is not UNSET: - field_dict["description"] = description return field_dict - def to_multipart(self) -> dict[str, Any]: - a_string = (None, str(self.a_string).encode(), "text/plain") + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] - file = self.file.to_tuple() + files.append(("a_string", (None, str(self.a_string).encode(), "text/plain"))) - description = ( - self.description - if isinstance(self.description, Unset) - else (None, str(self.description).encode(), "text/plain") - ) + for files_item_element in self.files: + files.append(("files", files_item_element.to_tuple())) - field_dict: dict[str, Any] = {} - for prop_name, prop in self.additional_properties.items(): - field_dict[prop_name] = (None, str(prop).encode(), "text/plain") + files.append(("description", (None, str(self.description).encode(), "text/plain"))) - field_dict.update( - { - "a_string": a_string, - "file": file, - } - ) - if description is not UNSET: - field_dict["description"] = description + for objects_item_element in self.objects: + files.append(("objects", (None, json.dumps(objects_item_element.to_dict()).encode(), "application/json"))) - return field_dict + for times_item_element in self.times: + files.append(("times", (None, times_item_element.isoformat().encode(), "text/plain"))) + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + return files @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.an_object import AnObject # noqa: PLC0415 + + d = dict(src_dict) a_string = d.pop("a_string") - file = File(payload=BytesIO(d.pop("file"))) + files = [] + _files = d.pop("files") + for files_item_data in _files: + files_item = File(payload=BytesIO(files_item_data)) + + files.append(files_item) + + description = d.pop("description") + + objects = [] + _objects = d.pop("objects") + for objects_item_data in _objects: + objects_item = AnObject.from_dict(objects_item_data) + + objects.append(objects_item) + + times = [] + _times = d.pop("times") + for times_item_data in _times: + times_item = datetime.datetime.fromisoformat(times_item_data) - description = d.pop("description", UNSET) + times.append(times_item) post_body_multipart_body = cls( a_string=a_string, - file=file, + files=files, description=description, + objects=objects, + times=times, ) post_body_multipart_body.additional_properties = d diff --git a/integration-tests/integration_tests/models/post_body_multipart_response_200.py b/integration-tests/integration_tests/models/post_body_multipart_response_200.py index 65f38c082..be63512d2 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_response_200.py +++ b/integration-tests/integration_tests/models/post_body_multipart_response_200.py @@ -1,8 +1,17 @@ -from typing import Any, TypeVar +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field +if TYPE_CHECKING: + from ..models.an_object import AnObject + from ..models.file import File + + T = TypeVar("T", bound="PostBodyMultipartResponse200") @@ -11,63 +20,90 @@ class PostBodyMultipartResponse200: """ Attributes: a_string (str): Echo of the 'a_string' input parameter from the form. - file_data (str): Echo of content of the 'file' input parameter from the form. description (str): Echo of the 'description' input parameter from the form. - file_name (str): The name of the file uploaded. - file_content_type (str): The content type of the file uploaded. + files (list[File]): + times (list[datetime.datetime]): + objects (list[AnObject]): """ a_string: str - file_data: str description: str - file_name: str - file_content_type: str + files: list[File] + times: list[datetime.datetime] + objects: list[AnObject] additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: a_string = self.a_string - file_data = self.file_data - description = self.description - file_name = self.file_name + files = [] + for files_item_data in self.files: + files_item = files_item_data.to_dict() + files.append(files_item) + + times = [] + for times_item_data in self.times: + times_item = times_item_data.isoformat() + times.append(times_item) - file_content_type = self.file_content_type + objects = [] + for objects_item_data in self.objects: + objects_item = objects_item_data.to_dict() + objects.append(objects_item) field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( { "a_string": a_string, - "file_data": file_data, "description": description, - "file_name": file_name, - "file_content_type": file_content_type, + "files": files, + "times": times, + "objects": objects, } ) return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() - a_string = d.pop("a_string") + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.an_object import AnObject # noqa: PLC0415 + from ..models.file import File # noqa: PLC0415 - file_data = d.pop("file_data") + d = dict(src_dict) + a_string = d.pop("a_string") description = d.pop("description") - file_name = d.pop("file_name") + files = [] + _files = d.pop("files") + for files_item_data in _files: + files_item = File.from_dict(files_item_data) + + files.append(files_item) + + times = [] + _times = d.pop("times") + for times_item_data in _times: + times_item = datetime.datetime.fromisoformat(times_item_data) + + times.append(times_item) + + objects = [] + _objects = d.pop("objects") + for objects_item_data in _objects: + objects_item = AnObject.from_dict(objects_item_data) - file_content_type = d.pop("file_content_type") + objects.append(objects_item) post_body_multipart_response_200 = cls( a_string=a_string, - file_data=file_data, description=description, - file_name=file_name, - file_content_type=file_content_type, + files=files, + times=times, + objects=objects, ) post_body_multipart_response_200.additional_properties = d diff --git a/integration-tests/integration_tests/models/post_parameters_header_response_200.py b/integration-tests/integration_tests/models/post_parameters_header_response_200.py index ff44f5644..d5baeabbd 100644 --- a/integration-tests/integration_tests/models/post_parameters_header_response_200.py +++ b/integration-tests/integration_tests/models/post_parameters_header_response_200.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from collections.abc import Mapping from typing import Any, TypeVar from attrs import define as _attrs_define @@ -45,8 +48,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) boolean = d.pop("boolean") string = d.pop("string") diff --git a/integration-tests/integration_tests/models/problem.py b/integration-tests/integration_tests/models/problem.py index f61f42b5d..a7f270874 100644 --- a/integration-tests/integration_tests/models/problem.py +++ b/integration-tests/integration_tests/models/problem.py @@ -1,4 +1,7 @@ -from typing import Any, TypeVar, Union +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -12,12 +15,12 @@ class Problem: """ Attributes: - parameter_name (Union[Unset, str]): - description (Union[Unset, str]): + parameter_name (str | Unset): + description (str | Unset): """ - parameter_name: Union[Unset, str] = UNSET - description: Union[Unset, str] = UNSET + parameter_name: str | Unset = UNSET + description: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -36,8 +39,8 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - d = src_dict.copy() + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) parameter_name = d.pop("parameter_name", UNSET) description = d.pop("description", UNSET) diff --git a/integration-tests/integration_tests/models/public_error.py b/integration-tests/integration_tests/models/public_error.py index 5e9d53c26..b4ca256d2 100644 --- a/integration-tests/integration_tests/models/public_error.py +++ b/integration-tests/integration_tests/models/public_error.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING, Any, TypeVar, Union, cast +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field @@ -16,35 +19,35 @@ class PublicError: """ Attributes: - errors (Union[Unset, list[str]]): - extra_parameters (Union[Unset, list[str]]): - invalid_parameters (Union[Unset, list['Problem']]): - missing_parameters (Union[Unset, list[str]]): + errors (list[str] | Unset): + extra_parameters (list[str] | Unset): + invalid_parameters (list[Problem] | Unset): + missing_parameters (list[str] | Unset): """ - errors: Union[Unset, list[str]] = UNSET - extra_parameters: Union[Unset, list[str]] = UNSET - invalid_parameters: Union[Unset, list["Problem"]] = UNSET - missing_parameters: Union[Unset, list[str]] = UNSET + errors: list[str] | Unset = UNSET + extra_parameters: list[str] | Unset = UNSET + invalid_parameters: list[Problem] | Unset = UNSET + missing_parameters: list[str] | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - errors: Union[Unset, list[str]] = UNSET + errors: list[str] | Unset = UNSET if not isinstance(self.errors, Unset): errors = self.errors - extra_parameters: Union[Unset, list[str]] = UNSET + extra_parameters: list[str] | Unset = UNSET if not isinstance(self.extra_parameters, Unset): extra_parameters = self.extra_parameters - invalid_parameters: Union[Unset, list[dict[str, Any]]] = UNSET + invalid_parameters: list[dict[str, Any]] | Unset = UNSET if not isinstance(self.invalid_parameters, Unset): invalid_parameters = [] for invalid_parameters_item_data in self.invalid_parameters: invalid_parameters_item = invalid_parameters_item_data.to_dict() invalid_parameters.append(invalid_parameters_item) - missing_parameters: Union[Unset, list[str]] = UNSET + missing_parameters: list[str] | Unset = UNSET if not isinstance(self.missing_parameters, Unset): missing_parameters = self.missing_parameters @@ -63,20 +66,22 @@ def to_dict(self) -> dict[str, Any]: return field_dict @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - from ..models.problem import Problem + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.problem import Problem # noqa: PLC0415 - d = src_dict.copy() + d = dict(src_dict) errors = cast(list[str], d.pop("errors", UNSET)) extra_parameters = cast(list[str], d.pop("extra_parameters", UNSET)) - invalid_parameters = [] _invalid_parameters = d.pop("invalid_parameters", UNSET) - for invalid_parameters_item_data in _invalid_parameters or []: - invalid_parameters_item = Problem.from_dict(invalid_parameters_item_data) + invalid_parameters: list[Problem] | Unset = UNSET + if _invalid_parameters is not UNSET: + invalid_parameters = [] + for invalid_parameters_item_data in _invalid_parameters: + invalid_parameters_item = Problem.from_dict(invalid_parameters_item_data) - invalid_parameters.append(invalid_parameters_item) + invalid_parameters.append(invalid_parameters_item) missing_parameters = cast(list[str], d.pop("missing_parameters", UNSET)) diff --git a/integration-tests/integration_tests/types.py b/integration-tests/integration_tests/types.py index b9ed58b8a..9090a2620 100644 --- a/integration-tests/integration_tests/types.py +++ b/integration-tests/integration_tests/types.py @@ -1,8 +1,8 @@ """Contains some shared types for properties""" -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from http import HTTPStatus -from typing import BinaryIO, Generic, Literal, Optional, TypeVar +from typing import IO, BinaryIO, Generic, Literal, TypeVar from attrs import define @@ -14,7 +14,15 @@ def __bool__(self) -> Literal[False]: UNSET: Unset = Unset() -FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]] +# The types that `httpx2.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] @define @@ -22,11 +30,11 @@ class File: """Contains information for file uploads""" payload: BinaryIO - file_name: Optional[str] = None - mime_type: Optional[str] = None + file_name: str | None = None + mime_type: str | None = None - def to_tuple(self) -> FileJsonType: - """Return a tuple representation that httpx will accept for multipart/form-data""" + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx2 will accept for multipart/form-data""" return self.file_name, self.payload, self.mime_type @@ -40,7 +48,7 @@ class Response(Generic[T]): status_code: HTTPStatus content: bytes headers: MutableMapping[str, str] - parsed: Optional[T] + parsed: T | None -__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"] +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/integration-tests/pdm.lock b/integration-tests/pdm.lock index f1501375a..1b030e9a0 100644 --- a/integration-tests/pdm.lock +++ b/integration-tests/pdm.lock @@ -4,49 +4,104 @@ [metadata] groups = ["default", "dev"] strategy = ["inherit_metadata"] -lock_version = "4.5.0" -content_hash = "sha256:a8195ab20cd4be2c783402cc1a2fd155d4eb5e8a855e3e1eb2dd171e039d7b3e" +lock_version = "4.5.1" +content_hash = "sha256:eb4eb7e74795ab92e4c226931cb38597587e39a9745cf55963cbd45f65f60c70" [[metadata.targets]] -requires_python = "~=3.8" +requires_python = "~=3.11" [[package]] name = "anyio" -version = "4.5.2" -requires_python = ">=3.8" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" +version = "4.16.0" +requires_python = ">=3.10" +summary = "High-level concurrency and networking framework on top of asyncio or Trio" groups = ["default"] +marker = "sys_platform != \"emscripten\"" dependencies = [ "exceptiongroup>=1.0.2; python_version < \"3.11\"", "idna>=2.8", - "sniffio>=1.1", - "typing-extensions>=4.1; python_version < \"3.11\"", + "typing-extensions>=4.16.0; python_version < \"3.15\"", ] files = [ - {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, - {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, + {file = "anyio-4.16.0-py3-none-any.whl", hash = "sha256:c66ae634e0b8228396b801741beb3294326d4b6102f7bba4041c0dfb58c29026"}, + {file = "anyio-4.16.0.tar.gz", hash = "sha256:59bb03251907983d1603d27c3087ddb1e93299ea2163adcdba37899a4fb12ffb"}, ] [[package]] -name = "attrs" -version = "24.3.0" -requires_python = ">=3.8" -summary = "Classes Without Boilerplate" -groups = ["default"] +name = "ast-serialize" +version = "0.13.1" +requires_python = ">=3.7" +summary = "Python bindings for mypy AST serialization" +groups = ["dev"] files = [ - {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, - {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, + {file = "ast_serialize-0.13.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3e129d44b0cffbea24a7f40117ce49e30993c3d119e584d4817ebd6f87246456"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:523d45bab16ddaf1399c04c4fe81bbf180f81e33c2f7bcbe76be56b0f7311ab4"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:638333fb8ae72c62753f9e794fda20290883ddc65b0d85e26e08f8bb227567ad"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc34c670747d85d4232834665e247009413eaebe47c69eaf60a5c439aaecaed9"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c8ebd419811c435812d242fb2a173e57dfede198c9ac9a0a584666ebc9b9331"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8899c038b1c15ae8b98e379057fb3ef398ceb0b21d1737a8156dd5a0923b7e51"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e14c4de98f965647a74e6eaf37e20207124e45451ccd36e84a6f6ebab45e0372"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3e8a3eff69af640db985360fef35073fb46956dfc3e5c614cf00920d2746dd"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:75d7750fe98f6f35a96c43aba8d98a2e623e22c778a0c17ab9ede92eb10f5d82"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e8ddd63e80fc280c2f790157386f27f9b9f6ccb9c4f8e803b863cbfb085769"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9221e09496fb942901993c1812ee87950cc0ecbb514c7c0f1f5fa6d78f701ed8"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7a5c38bdb93669c596f94fb7204dd6ebe4fadb19058ac9a2faf2948c2a175f96"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:acf4e9f055d75058ab01536da330648746f2f82b7697a85230547f2d5ebe895c"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87427913c325adc811e81e7c9a757b061f47ef1e29114ac479aa0953a2687a98"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ad88921f95b92bc144d9ad059a0e16f8c1ae69f5239f2d3c6536fd2774f73dfe"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f8b17e2921e56621bcb46792bd3e0b82ca3b85de78d5936e7b8174ba1758b0aa"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win32.whl", hash = "sha256:2f9a9ce8b0994f757b941f7f7fe384a6666b4162b941ebc5c248081baea18e47"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5d99910e6b9a9b9360bcca2ae99aee36cc912ce058df2a57d1c33099b6cb2593"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a789cbe14d0a0e9ec9b32a456e5abc81ad8799479c3cfd88a29e3442262aebe6"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:23d768369b89d551077dc173da6892fad00443338417523b6732f4e8ba4a7ed5"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:68f8829d114dab6c9db4a8bbb8d0e342958faee1b5e15a29022678df639960f8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e385b17d5bde892a6184003a9aff3518387908b73f8f5ea539313c07c8c0ede"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02686b281ec9b8e9614f6f0db27e12e4cac515f336c5d05b678bc9eb73ff2af5"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8ab2c2585b2e22e48cd4aff2136fce4eab4ced569b85f150ef9a5a5d6f34d49"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c652d9f5d52d8b81eff3ff2c95c9dd0f281de45f6ad5d3fa95527550c396454a"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121e1d08d58b75c15491cc3003609cbad71ac2b0016043c36d30a6b2032b6605"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:30cf89b6a6844542d116c758725d13de5b97b9410a51ee577715f761270d0c1c"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:732aaae9e498c973fc63fd7f308009b57c4afa6ff59d0b674d894e57d0bca01a"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d9c1a920b2aa0c1e016bed7721689717034ca0dba64ac3a3ff243b003d4e8dd8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:847cad4c22953823f5e76d0b126c6f9da670cfa45b1df41b9324e22189d77611"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:aca07de3ca8e3d9f2b716c3c31f4f384fe4bd2d03e09912417cff7a3e08036f7"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:5db12d843d931b9d1f1519ce66b9cbfe72d8563b75c1204d255322254d1964dd"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:fb8021f4dc3296de18737e3b8b34994411da725374d74a0b9eb32a738f2a23d0"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:0d764913374ba21df8cd618045b371ed104ce55c6276de7c59e554a4f5ba26fd"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:2bad302ae2427454ef9dab71bd5c3ad67188860c39d36daee38f72031edb39c4"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:032b09d86c785c3984e975378b7861f2a68ac44e22ba7b07b93a536b2524eeb8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:929eda0c419e622ab49df3a4c45f81c57dcc10874bbad55d0470a4a03324dac3"}, + {file = "ast_serialize-0.13.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bf53a3ba1cbe45399fc1e4ccd684b8f2a11cca450f89706b79122f478534c6f5"}, + {file = "ast_serialize-0.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4bcf1d5f4eefecaa325a34ecf079b2741189e7d0821523f92cb343a389a67489"}, + {file = "ast_serialize-0.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1b158f1c9e3a0d386913924c57995d6df78224868a4d0f215bdd41587c97b34a"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a6201b69ce42e919f2fd9a559138d5410b5d8afff7c3a6b1f500b0481ff7d8"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eb0be64c9b8f1ed26637eef345abe75ea90351c92c85a8aec7838b3af971011"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2427bddfca208e84de700044fadf9aef2769a70bae740a1bdabb89ee9e1117"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88df960e67184a71874e5b2ee014f87d1254a0dbc1f903c7bd7b1ab7733ecaef"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f99786e0d62082a5b121b3437f3167562133711df22128de1e9886f57f0b226"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:a01b515b25a03184eff4acef25b981ae007916379682e908fc8785b342a0bf3e"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29422d7bcd9e7f39411571c36cec45ce8a7a9b3ad4b2e7aa64497c15a9e397f6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ffb0f548a7dc126067fdfb7b2325ab5dfcdeecc29faf5e0f0461c96d6ebfa3d"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2876444a5f87a7c76f371af3c07c2fda563a08bc8af52568df812e8aba4bf568"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:885a62ec6481ce58ab24c6357360d7a82023b62dd7e5e7ceccf59e9662a3272b"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2015fd0165e463aac3b1aadc6a1fa1e5820c14ca950821f4fe02f8a3f6a388d6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:eb41e1906502ac80d24ff343f45008552a3f5b5158607959fe6a07dbfc6eb037"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ca9acc67f796aa2554fa3037dccc022a4b08b38b2ad153edfac9c9bb7cb1f3c6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win32.whl", hash = "sha256:4f98da562e5901d69c7dd30885594c5438f859edf354e9ac7517d188fb026c3a"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:ddf7cbc9c8abf70102c4a088efe154bb86bfb825b0a10e556d2b15972cf99bd7"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win_arm64.whl", hash = "sha256:48f11d96c9ec71119612f8e79a8c55c12f2b136716d68347433c561d27a25f43"}, + {file = "ast_serialize-0.13.1.tar.gz", hash = "sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025"}, ] [[package]] -name = "certifi" -version = "2024.12.14" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." +name = "attrs" +version = "26.1.0" +requires_python = ">=3.9" +summary = "Classes Without Boilerplate" groups = ["default"] files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] [[package]] @@ -61,283 +116,380 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "exceptiongroup" -version = "1.2.2" -requires_python = ">=3.7" -summary = "Backport of PEP 654 (exception groups)" -groups = ["default", "dev"] -marker = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - [[package]] name = "h11" -version = "0.14.0" -requires_python = ">=3.7" +version = "0.16.0" +requires_python = ">=3.8" summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" groups = ["default"] -dependencies = [ - "typing-extensions; python_version < \"3.8\"", -] +marker = "sys_platform != \"emscripten\"" files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {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.7" -requires_python = ">=3.8" +name = "httpcore2" +version = "2.16.0" +requires_python = ">=3.11" summary = "A minimal low-level HTTP client." groups = ["default"] +marker = "sys_platform != \"emscripten\"" dependencies = [ - "certifi", - "h11<0.15,>=0.13", + "h11>=0.16", + "truststore>=0.10", ] files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, + {file = "httpcore2-2.16.0-py3-none-any.whl", hash = "sha256:5c4358319f2602d984ad93182c9fee5160f22e9da7babdda870a5e7c4e2dc34f"}, + {file = "httpcore2-2.16.0.tar.gz", hash = "sha256:c72cd0413563b17e1cd8354677dff887466f8ec2551679759e93126534428643"}, ] [[package]] -name = "httpx" -version = "0.28.1" -requires_python = ">=3.8" +name = "httpx2" +version = "2.16.0" +requires_python = ">=3.11" summary = "The next generation HTTP client." groups = ["default"] dependencies = [ - "anyio", - "certifi", - "httpcore==1.*", - "idna", + "anyio>=4.10; sys_platform != \"emscripten\"", + "httpcore2==2.16.0; sys_platform != \"emscripten\"", + "httpx2-jsfetch; sys_platform == \"emscripten\" and python_version >= \"3.12\"", + "idna>=3.18", + "truststore>=0.10; sys_platform != \"emscripten\"", + "typing-extensions>=4.5.0; python_version < \"3.13\"", ] files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, + {file = "httpx2-2.16.0-py3-none-any.whl", hash = "sha256:fd2c5d7909e8d86fe92d53020c310ca0e4e431b838c207c5754b9bbf0308d49a"}, + {file = "httpx2-2.16.0.tar.gz", hash = "sha256:0d54c98128e8f2ff7fbbde0fca8c359e5f032edeaaaac443cdb7f31f685bcd7f"}, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +requires_python = ">=3.12" +summary = "httpx2 transports for Emscripten/Pyodide, backed by the JavaScript fetch API." +groups = ["default"] +marker = "sys_platform == \"emscripten\" and python_version >= \"3.12\"" +files = [ + {file = "httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32"}, + {file = "httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60"}, ] [[package]] name = "idna" -version = "3.10" -requires_python = ">=3.6" +version = "3.20" +requires_python = ">=3.9" summary = "Internationalized Domain Names in Applications (IDNA)" groups = ["default"] files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, + {file = "idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c"}, + {file = "idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44"}, ] [[package]] name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" +version = "2.3.1" +requires_python = ">=3.10" summary = "brain-dead simple config-ini parsing" groups = ["dev"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7"}, + {file = "iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960"}, +] + +[[package]] +name = "librt" +version = "0.16.0" +requires_python = ">=3.9" +summary = "Mypyc runtime library" +groups = ["dev"] +marker = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18"}, + {file = "librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602"}, + {file = "librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96"}, + {file = "librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7"}, + {file = "librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d"}, + {file = "librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678"}, + {file = "librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742"}, + {file = "librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07"}, + {file = "librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b"}, + {file = "librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a"}, + {file = "librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940"}, + {file = "librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc"}, + {file = "librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a"}, + {file = "librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6"}, + {file = "librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e"}, + {file = "librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d"}, + {file = "librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4"}, + {file = "librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92"}, + {file = "librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91"}, + {file = "librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1"}, + {file = "librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667"}, + {file = "librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152"}, + {file = "librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839"}, + {file = "librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7"}, + {file = "librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41"}, + {file = "librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99"}, + {file = "librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc"}, + {file = "librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1"}, + {file = "librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7"}, + {file = "librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d"}, + {file = "librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b"}, + {file = "librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff"}, + {file = "librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d"}, + {file = "librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42"}, + {file = "librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a"}, + {file = "librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3"}, + {file = "librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971"}, + {file = "librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560"}, + {file = "librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b"}, + {file = "librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba"}, + {file = "librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9"}, + {file = "librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc"}, + {file = "librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866"}, + {file = "librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc"}, ] [[package]] name = "mypy" -version = "1.14.0" -requires_python = ">=3.8" +version = "2.4.1" +requires_python = ">=3.10" summary = "Optional static typing for Python" groups = ["dev"] dependencies = [ + "ast-serialize<1.0.0,>=0.12.1", + "librt>=0.16.0; platform_python_implementation != \"PyPy\"", "mypy-extensions>=1.0.0", + "pathspec>=1.0.0", "tomli>=1.1.0; python_version < \"3.11\"", - "typing-extensions>=4.6.0", + "typing-extensions>=4.14.0; python_version >= \"3.15\"", + "typing-extensions>=4.6.0; python_version < \"3.15\"", ] files = [ - {file = "mypy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e971c1c667007f9f2b397ffa80fa8e1e0adccff336e5e77e74cb5f22868bee87"}, - {file = "mypy-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e86aaeaa3221a278c66d3d673b297232947d873773d61ca3ee0e28b2ff027179"}, - {file = "mypy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1628c5c3ce823d296e41e2984ff88c5861499041cb416a8809615d0c1f41740e"}, - {file = "mypy-1.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fadb29b77fc14a0dd81304ed73c828c3e5cde0016c7e668a86a3e0dfc9f3af3"}, - {file = "mypy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:3fa76988dc760da377c1e5069200a50d9eaaccf34f4ea18428a3337034ab5a44"}, - {file = "mypy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e73c8a154eed31db3445fe28f63ad2d97b674b911c00191416cf7f6459fd49a"}, - {file = "mypy-1.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:273e70fcb2e38c5405a188425aa60b984ffdcef65d6c746ea5813024b68c73dc"}, - {file = "mypy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1daca283d732943731a6a9f20fdbcaa927f160bc51602b1d4ef880a6fb252015"}, - {file = "mypy-1.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7e68047bedb04c1c25bba9901ea46ff60d5eaac2d71b1f2161f33107e2b368eb"}, - {file = "mypy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:7a52f26b9c9b1664a60d87675f3bae00b5c7f2806e0c2800545a32c325920bcc"}, - {file = "mypy-1.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d5326ab70a6db8e856d59ad4cb72741124950cbbf32e7b70e30166ba7bbf61dd"}, - {file = "mypy-1.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf4ec4980bec1e0e24e5075f449d014011527ae0055884c7e3abc6a99cd2c7f1"}, - {file = "mypy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:390dfb898239c25289495500f12fa73aa7f24a4c6d90ccdc165762462b998d63"}, - {file = "mypy-1.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e026d55ddcd76e29e87865c08cbe2d0104e2b3153a523c529de584759379d3d"}, - {file = "mypy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:585ed36031d0b3ee362e5107ef449a8b5dfd4e9c90ccbe36414ee405ee6b32ba"}, - {file = "mypy-1.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9f6f4c0b27401d14c483c622bc5105eff3911634d576bbdf6695b9a7c1ba741"}, - {file = "mypy-1.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b2280cedcb312c7a79f5001ae5325582d0d339bce684e4a529069d0e7ca1e7"}, - {file = "mypy-1.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:342de51c48bab326bfc77ce056ba08c076d82ce4f5a86621f972ed39970f94d8"}, - {file = "mypy-1.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00df23b42e533e02a6f0055e54de9a6ed491cd8b7ea738647364fd3a39ea7efc"}, - {file = "mypy-1.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e8c8387e5d9dff80e7daf961df357c80e694e942d9755f3ad77d69b0957b8e3f"}, - {file = "mypy-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b16738b1d80ec4334654e89e798eb705ac0c36c8a5c4798496cd3623aa02286"}, - {file = "mypy-1.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10065fcebb7c66df04b05fc799a854b1ae24d9963c8bb27e9064a9bdb43aa8ad"}, - {file = "mypy-1.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbb7d683fa6bdecaa106e8368aa973ecc0ddb79a9eaeb4b821591ecd07e9e03c"}, - {file = "mypy-1.14.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3498cb55448dc5533e438cd13d6ddd28654559c8c4d1fd4b5ca57a31b81bac01"}, - {file = "mypy-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:c7b243408ea43755f3a21a0a08e5c5ae30eddb4c58a80f415ca6b118816e60aa"}, - {file = "mypy-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14117b9da3305b39860d0aa34b8f1ff74d209a368829a584eb77524389a9c13e"}, - {file = "mypy-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af98c5a958f9c37404bd4eef2f920b94874507e146ed6ee559f185b8809c44cc"}, - {file = "mypy-1.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b343a1d3989547024377c2ba0dca9c74a2428ad6ed24283c213af8dbb0710b"}, - {file = "mypy-1.14.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cdb5563c1726c85fb201be383168f8c866032db95e1095600806625b3a648cb7"}, - {file = "mypy-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:74e925649c1ee0a79aa7448baf2668d81cc287dc5782cff6a04ee93f40fb8d3f"}, - {file = "mypy-1.14.0-py3-none-any.whl", hash = "sha256:2238d7f93fc4027ed1efc944507683df3ba406445a2b6c96e79666a045aadfab"}, - {file = "mypy-1.14.0.tar.gz", hash = "sha256:822dbd184d4a9804df5a7d5335a68cf7662930e70b8c1bc976645d1509f9a9d6"}, + {file = "mypy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:79a1e79199e980fb41f219cb3d4746d25c64b0b623840d3ed57df1cdc13a6502"}, + {file = "mypy-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e18d0da3898b3bcd16192f15a206e7b8b8dd0f4a0b3e51aa5bd5c2afe749e2c"}, + {file = "mypy-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69a15fc2e75858d6b15e91ed3a0dcff1a533265183b64734138791fe0f49aa5"}, + {file = "mypy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70971cc0bcb1d3f03d2eb68fa430a6481ccc03558dc001d9534d7e5fa274c392"}, + {file = "mypy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f7225951a277dd0d6add900731d487174b7d24ac133c09d670ed63b4d8cb90d"}, + {file = "mypy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:be4c03f33c55500191df2ed4a9b5e7edbdd1fff067fe361a5a9b1b5d3262da6e"}, + {file = "mypy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d63737426a048a27705a4eedb828669f4bc0873612a1b46b0b9fe892b2ebaaaf"}, + {file = "mypy-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7344869cad05f073b8cae2826774fa0161e3eb2f5c685b76813574008d77d750"}, + {file = "mypy-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a20ff6cd2af7a670c64f089bfa5fbcebe8ff3bf896c5e6ac7c9862c7960e4494"}, + {file = "mypy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ed1195502cca32be5ef756b1693477f4991db2f2e34eaa90cc4ad6bea4f9531"}, + {file = "mypy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c194c5d878130fca1ae662dfb1feb6274afbab0b40032c80147377e0ea803d5"}, + {file = "mypy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:71eda06e5bae51976ce869fda429159c9cec678bd02f25bb197f0265c8f991be"}, + {file = "mypy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f1613bfe6726169a5680ec3dad5fe1f6d96285c12aded51ad432eadedd39d45"}, + {file = "mypy-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:680ba5d9f0e3fe0c746b3d14df15f2a718e4bc308a1145616a1ea691ce7c354b"}, + {file = "mypy-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb70621535c22fd4b1a0450d37ed1d6c5eb39656a1cebfb189084c12de54ba7"}, + {file = "mypy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4af5ad558033bf71a44a1be149ed61633522824f84c1b6bc2445d3fe2d2a5296"}, + {file = "mypy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:acdd82cb3a2eb10720e87706a128d75785d005a8c47e36a4973221524f41fc56"}, + {file = "mypy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:133886ef1a7a8cf673df55d865e41580c6c46aa9b8e31e1325dfe315c038f745"}, + {file = "mypy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c45485061ba08f700acc12cfc8b177e6997e6bc353605babfc0abb081e079456"}, + {file = "mypy-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727890a32f1858c09549eb55a8b234fe990d460450b28f03e8213ef925f1247c"}, + {file = "mypy-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:beb254fec368c4e7f7f82a5f310c6470b893ec88a90b66426da971c6826b0453"}, + {file = "mypy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ec416d5d2ae689e681f5727e328723f18d8b5dc8c903a146b7851dc5727aad8a"}, + {file = "mypy-2.4.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:0e95ad0bec03ced9a73b42514208abd24c789e67f0e1eba476e276dd7a2dea3a"}, + {file = "mypy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:51f742d4154824837ca6e92aa81f00eec222700a3c337bd258e5562a3880d75a"}, + {file = "mypy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:f44dd4c5ab22df21206dd926c98381ebdbf5bfd696df37dd38ded2d23143fcb6"}, + {file = "mypy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2875280687e64740e6ddcef43bd4c5852ad45ff34549740f6aea3f0373870b93"}, + {file = "mypy-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea0714d2bccaf6d010c970e1d3ba2c9c2add79d2030aad2ef03167d788948276"}, + {file = "mypy-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c6ff0e7ea546943001a91639c9f0c64b60d239268fa5d29e4a6c87f82c2373"}, + {file = "mypy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a5b5da107dc2c93fb0a22f7a45dbaea5dc131afc7056b5468d78e819f9a138e6"}, + {file = "mypy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c3a4d0c47053cc90035dd3a2c321112b593ad42935d0cbfd747288e1564fa8ca"}, + {file = "mypy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0c5bf267645351887b72903fd2f5fbddcf85234e0e6c4ad94e2fcfb4f0c3f971"}, + {file = "mypy-2.4.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:db599d03e7d60d15f2e0a11665c18bc52a88699fcab990fdf8cecd5be8ac801c"}, + {file = "mypy-2.4.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6c7210e40dd423af7d65cbe428b5906d11dc60659337bdae4e5fdf02ce32ed7"}, + {file = "mypy-2.4.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2d4a51db92059eecc2c33b2a5194b28585ebfd917292c015fe87a77dbff6cd2"}, + {file = "mypy-2.4.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:01b7718552d017f88d1151f3cc9d6556d778a90bd58060a2c3d87d070f9099fb"}, + {file = "mypy-2.4.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:3467b1d13000812ab85dc2a3834b9379ec77798ca1bcabe434e23fb21fead692"}, + {file = "mypy-2.4.1-cp315-cp315-win_amd64.whl", hash = "sha256:080cf64a60b90a73435542feecf4171904cc5710658165b40c6869115d4aa100"}, + {file = "mypy-2.4.1-cp315-cp315-win_arm64.whl", hash = "sha256:f9118a3f50de264b614861bb11814833a742138ac3c8e2e49a860b6977fb2704"}, + {file = "mypy-2.4.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9d7dafd56179d296651df9e1844b91c9517c379398f017a2a435d077a28943c1"}, + {file = "mypy-2.4.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d923cec43316f0e6736b8298fd00abeaaf348a091c20e7296b3aebceecb63eca"}, + {file = "mypy-2.4.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6e081b466e88d4343adffe11aff0f02df34b5b9070b4b700053a0b7294c0be1"}, + {file = "mypy-2.4.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:70196470ac300f444a302a738541101893c2c57804078b33690286a7899149e9"}, + {file = "mypy-2.4.1-cp315-cp315t-win_amd64.whl", hash = "sha256:de95648e0a6d46fa9929b956ed2c87c5dc9933d4d59945e6390e9d753c74e7c1"}, + {file = "mypy-2.4.1-cp315-cp315t-win_arm64.whl", hash = "sha256:ef8d5be90032261197ac388d945d8ff4f476c95880798a1911760af64ddfe71f"}, + {file = "mypy-2.4.1-py3-none-any.whl", hash = "sha256:1784648d6ec8c2b5465e3f26241b845a0b3cb4e56c993c64abe16989e7698999"}, + {file = "mypy-2.4.1.tar.gz", hash = "sha256:63b590965691e540a335fa74dadf31a7069142f6b72e234689bbd244edf55623"}, ] [[package]] name = "mypy-extensions" -version = "1.0.0" -requires_python = ">=3.5" +version = "1.2.0" +requires_python = ">=3.10" summary = "Type system extensions for programs checked with the mypy type checker." groups = ["dev"] files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, + {file = "mypy_extensions-1.2.0-py3-none-any.whl", hash = "sha256:993e7a20aa5671eb41a7132cf555af30062da99729601601b9a8382b11cae768"}, + {file = "mypy_extensions-1.2.0.tar.gz", hash = "sha256:c6e275a07f1c2fc096b62940452d0eaa3f3af7de45d704787866240a7c6d5a51"}, ] [[package]] name = "packaging" -version = "24.2" -requires_python = ">=3.8" +version = "26.3" +requires_python = ">=3.9" summary = "Core utilities for Python packages" groups = ["dev"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] -name = "pluggy" -version = "1.5.0" -requires_python = ">=3.8" -summary = "plugin and hook calling mechanisms for python" +name = "pathspec" +version = "1.1.1" +requires_python = ">=3.9" +summary = "Utility library for gitignore style pattern matching of file paths." groups = ["dev"] files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, + {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, ] [[package]] -name = "pytest" -version = "8.3.4" -requires_python = ">=3.8" -summary = "pytest: simple powerful testing with Python" +name = "pluggy" +version = "1.7.0" +requires_python = ">=3.10" +summary = "plugin and hook calling mechanisms for python" groups = ["dev"] -dependencies = [ - "colorama; sys_platform == \"win32\"", - "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", - "iniconfig", - "packaging", - "pluggy<2,>=1.5", - "tomli>=1; python_version < \"3.11\"", -] files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, + {file = "pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec"}, + {file = "pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8"}, ] [[package]] -name = "pytest-asyncio" -version = "0.24.0" -requires_python = ">=3.8" -summary = "Pytest support for asyncio" +name = "pygments" +version = "2.21.0" +requires_python = ">=3.9" +summary = "Pygments is a syntax highlighting package written in Python." groups = ["dev"] -dependencies = [ - "pytest<9,>=8.2", -] files = [ - {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, - {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, ] [[package]] -name = "python-dateutil" -version = "2.9.0.post0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -groups = ["default"] +name = "pytest" +version = "9.2.0" +requires_python = ">=3.10" +summary = "pytest: simple powerful testing with Python" +groups = ["dev"] dependencies = [ - "six>=1.5", + "colorama>=0.4; sys_platform == \"win32\"", + "exceptiongroup>=1; python_version < \"3.11\"", + "iniconfig>=2", + "packaging>=24", + "pluggy<2,>=1.6", + "pygments>=2.15", + "tomli>=2; python_version < \"3.11\"", ] 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"}, + {file = "pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588"}, + {file = "pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b"}, ] [[package]] -name = "six" -version = "1.17.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Python 2 and 3 compatibility utilities" -groups = ["default"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +name = "pytest-asyncio" +version = "1.4.1.post1" +requires_python = ">=3.10" +summary = "Pytest support for asyncio" +groups = ["dev"] +dependencies = [ + "backports-asyncio-runner<2,>=1.1; python_version < \"3.11\"", + "pytest>=8.4", + "typing-extensions>=4.12; python_version < \"3.13\"", ] - -[[package]] -name = "sniffio" -version = "1.3.1" -requires_python = ">=3.7" -summary = "Sniff out which async library your code is running under" -groups = ["default"] files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, + {file = "pytest_asyncio-1.4.1.post1-py3-none-any.whl", hash = "sha256:0755e488cc0a2b9bd2eab7292cc6390d4cbb8390f1020745647bae5a708e0165"}, + {file = "pytest_asyncio-1.4.1.post1.tar.gz", hash = "sha256:f582d9ea4a1c082195abe8bd29a1fe106aeb1cd435e3dad2d3bc2749935c36a2"}, ] [[package]] -name = "tomli" -version = "2.2.1" -requires_python = ">=3.8" -summary = "A lil' TOML parser" -groups = ["dev"] -marker = "python_version < \"3.11\"" +name = "truststore" +version = "0.10.5" +requires_python = ">=3.10" +summary = "Verify certificates using native system trust stores" +groups = ["default"] +marker = "sys_platform != \"emscripten\"" files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, + {file = "truststore-0.10.5-py3-none-any.whl", hash = "sha256:9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c"}, + {file = "truststore-0.10.5.tar.gz", hash = "sha256:30d36967ccaded5cbb38d602c433f53600036c79d502f4533a49b60a03bbefcd"}, ] [[package]] name = "typing-extensions" -version = "4.12.2" -requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.16.0" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" groups = ["default", "dev"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] diff --git a/integration-tests/pdm.minimal.lock b/integration-tests/pdm.minimal.lock new file mode 100644 index 000000000..9838d01d1 --- /dev/null +++ b/integration-tests/pdm.minimal.lock @@ -0,0 +1,252 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev"] +strategy = ["direct_minimal_versions", "inherit_metadata"] +lock_version = "4.5.1" +content_hash = "sha256:eb4eb7e74795ab92e4c226931cb38597587e39a9745cf55963cbd45f65f60c70" + +[[metadata.targets]] +requires_python = "~=3.11" + +[[package]] +name = "anyio" +version = "4.13.0" +requires_python = ">=3.10" +summary = "High-level concurrency and networking framework on top of asyncio or Trio" +groups = ["default"] +marker = "sys_platform != \"emscripten\"" +dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "typing-extensions>=4.5; python_version < \"3.13\"", +] +files = [ + {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, + {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, +] + +[[package]] +name = "attrs" +version = "22.2.0" +requires_python = ">=3.6" +summary = "Classes Without Boilerplate" +groups = ["default"] +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["dev"] +marker = "sys_platform == \"win32\"" +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 = "h11" +version = "0.16.0" +requires_python = ">=3.8" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["default"] +marker = "sys_platform != \"emscripten\"" +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 = "httpcore2" +version = "2.13.1" +requires_python = ">=3.10" +summary = "A minimal low-level HTTP client." +groups = ["default"] +marker = "sys_platform != \"emscripten\"" +dependencies = [ + "h11>=0.16", + "truststore>=0.10", +] +files = [ + {file = "httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a"}, + {file = "httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103"}, +] + +[[package]] +name = "httpx2" +version = "2.13.1" +requires_python = ">=3.10" +summary = "The next generation HTTP client." +groups = ["default"] +dependencies = [ + "anyio>=4.10; sys_platform != \"emscripten\"", + "httpcore2==2.13.1; sys_platform != \"emscripten\"", + "httpx2-jsfetch; sys_platform == \"emscripten\" and python_version >= \"3.12\"", + "idna>=3.18", + "truststore>=0.10; sys_platform != \"emscripten\"", + "typing-extensions>=4.5.0; python_version < \"3.13\"", +] +files = [ + {file = "httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4"}, + {file = "httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa"}, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +requires_python = ">=3.12" +summary = "httpx2 transports for Emscripten/Pyodide, backed by the JavaScript fetch API." +groups = ["default"] +marker = "sys_platform == \"emscripten\" and python_version >= \"3.12\"" +files = [ + {file = "httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32"}, + {file = "httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60"}, +] + +[[package]] +name = "idna" +version = "3.20" +requires_python = ">=3.9" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default"] +files = [ + {file = "idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c"}, + {file = "idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44"}, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +requires_python = ">=3.10" +summary = "brain-dead simple config-ini parsing" +groups = ["dev"] +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" +requires_python = ">=3.8" +summary = "Optional static typing for Python" +groups = ["dev"] +dependencies = [ + "mypy-extensions>=1.0.0", + "tomli>=1.1.0; python_version < \"3.11\"", + "typing-extensions>=4.6.0", +] +files = [ + {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-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +requires_python = ">=3.8" +summary = "Type system extensions for programs checked with the mypy type checker." +groups = ["dev"] +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.2" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["dev"] +files = [ + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +requires_python = ">=3.9" +summary = "plugin and hook calling mechanisms for python" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[[package]] +name = "pytest" +version = "8.0.1" +requires_python = ">=3.8" +summary = "pytest: simple powerful testing with Python" +groups = ["dev"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2.0,>=1.3.0", + "tomli>=1.0.0; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"}, + {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"}, +] + +[[package]] +name = "pytest-asyncio" +version = "0.23.5" +requires_python = ">=3.8" +summary = "Pytest support for asyncio" +groups = ["dev"] +dependencies = [ + "pytest<9,>=7.0.0", +] +files = [ + {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, + {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, +] + +[[package]] +name = "truststore" +version = "0.10.5" +requires_python = ">=3.10" +summary = "Verify certificates using native system trust stores" +groups = ["default"] +marker = "sys_platform != \"emscripten\"" +files = [ + {file = "truststore-0.10.5-py3-none-any.whl", hash = "sha256:9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c"}, + {file = "truststore-0.10.5.tar.gz", hash = "sha256:30d36967ccaded5cbb38d602c433f53600036c79d502f4533a49b60a03bbefcd"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" +groups = ["default", "dev"] +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"}, +] diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml index cbcff74c6..591a62dc9 100644 --- a/integration-tests/pyproject.toml +++ b/integration-tests/pyproject.toml @@ -1,14 +1,13 @@ [project] name = "integration-tests" -version = "0.0.1" +version = "0.1.0" description = "A client library for accessing OpenAPI Test Server" authors = [] readme = "README.md" -requires-python = ">=3.9,<4.0" +requires-python = ">=3.11,<4.0" dependencies = [ - "httpx>=0.20.0,<0.29.0", + "httpx2>=2.13.1,<3", "attrs>=22.2.0", - "python-dateutil>=2.8.0", ] [tool.pdm] @@ -16,17 +15,20 @@ distribution = true [tool.pdm.dev-dependencies] dev = [ - "pytest", - "mypy", + "pytest>8", + "mypy>=1.13", "pytest-asyncio>=0.23.5", ] [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" - + [tool.ruff] line-length = 120 [tool.ruff.lint] select = ["F", "I"] + +[tool.mypy] +# Just to get mypy to _not_ look at the parent directory's config diff --git a/integration-tests/tests/test_api/test_body/test_post_body_multipart.py b/integration-tests/tests/test_api/test_body/test_post_body_multipart.py index 54498ebc5..fbb87a1d0 100644 --- a/integration-tests/tests/test_api/test_body/test_post_body_multipart.py +++ b/integration-tests/tests/test_api/test_body/test_post_body_multipart.py @@ -1,3 +1,4 @@ +from datetime import UTC, datetime, timedelta from io import BytesIO from typing import Any @@ -5,49 +6,67 @@ from integration_tests.api.body import post_body_multipart from integration_tests.client import Client +from integration_tests.models import AnObject, PublicError from integration_tests.models.post_body_multipart_body import PostBodyMultipartBody from integration_tests.models.post_body_multipart_response_200 import PostBodyMultipartResponse200 -from integration_tests.types import File +from integration_tests.types import File, Response + +body = PostBodyMultipartBody( + a_string="a test string", + description="super descriptive thing", + files=[ + File( + payload=BytesIO(b"some file content"), + file_name="cool_stuff.txt", + mime_type="application/openapi-python-client", + ), + File( + payload=BytesIO(b"more file content"), + file_name=None, + mime_type=None, + ), + ], + times=[datetime.now(UTC) - timedelta(days=1), datetime.now(UTC)], + objects=[ + AnObject( + an_int=1, + a_float=2.3, + ), + AnObject( + an_int=4, + a_float=5.6, + ), + ], +) -def test(client: Client) -> None: - a_string = "a test string" - payload = b"some file content" - file_name = "cool_stuff.txt" - mime_type = "application/openapi-python-client" - description = "super descriptive thing" +def check_response(response: Response[PostBodyMultipartResponse200 | PublicError]) -> None: + content = response.parsed + if not isinstance(content, PostBodyMultipartResponse200): + raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") + + assert content.a_string == body.a_string + assert content.description == body.description + assert content.times == body.times + assert content.objects == body.objects + assert len(content.files) == len(body.files) + for i, file in enumerate(content.files): + body.files[i].payload.seek(0) + assert file.data == body.files[i].payload.read().decode() + assert file.name == body.files[i].file_name + assert file.content_type == body.files[i].mime_type + +def test(client: Client) -> None: response = post_body_multipart.sync_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), - description=description, - ), + body=body, ) - content = response.parsed - if not isinstance(content, PostBodyMultipartResponse200): - raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") - - assert content.a_string == a_string - assert content.file_name == file_name - assert content.file_content_type == mime_type - assert content.file_data.encode() == payload - assert content.description == description + check_response(response) def test_custom_hooks() -> None: - a_string = "a test string" - payload = b"some file content" - file_name = "cool_stuff.txt" - mime_type = "application/openapi-python-client" - description = "super descriptive thing" - request_hook_called = False response_hook_called = False @@ -65,15 +84,7 @@ def log_response(*_: Any, **__: Any) -> None: post_body_multipart.sync_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), - description=description, - ), + body=body, ) assert request_hook_called @@ -81,149 +92,51 @@ def log_response(*_: Any, **__: Any) -> None: def test_context_manager(client: Client) -> None: - a_string = "a test string" - payload = b"some file content" - file_name = "cool_stuff.txt" - mime_type = "application/openapi-python-client" - description = "super descriptive thing" - with client as client: post_body_multipart.sync_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), - description=description, - ), + body=body, ) response = post_body_multipart.sync_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), - description=description, - ), + body=body, ) with pytest.raises(RuntimeError): post_body_multipart.sync_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), - description=description, - ), + body=body, ) - content = response.parsed - if not isinstance(content, PostBodyMultipartResponse200): - raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") - - assert content.a_string == a_string - assert content.file_name == file_name - assert content.file_content_type == mime_type - assert content.file_data.encode() == payload - assert content.description == description + check_response(response) @pytest.mark.asyncio async def test_async(client: Client) -> None: - a_string = "a test string" - payload = b"some file content" - file_name = "cool_stuff.txt" - mime_type = "application/openapi-python-client" - description = "super descriptive thing" - response = await post_body_multipart.asyncio_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), - description=description, - ), + body=body, ) - content = response.parsed - if not isinstance(content, PostBodyMultipartResponse200): - raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") - - assert content.a_string == a_string - assert content.file_name == file_name - assert content.file_content_type == mime_type - assert content.file_data.encode() == payload - assert content.description == description + check_response(response) @pytest.mark.asyncio async def test_async_context_manager(client: Client) -> None: - a_string = "a test string" - payload = b"some file content" - file_name = "cool_stuff.txt" - mime_type = "application/openapi-python-client" - description = "super descriptive thing" - async with client as client: await post_body_multipart.asyncio_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), - description=description, - ), + body=body, ) response = await post_body_multipart.asyncio_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), - description=description, - ), + body=body, ) with pytest.raises(RuntimeError): await post_body_multipart.asyncio_detailed( client=client, - body=PostBodyMultipartBody( - a_string=a_string, - file=File( - payload=BytesIO(payload), - file_name=file_name, - mime_type=mime_type, - ), - description=description, - ), + body=body, ) - content = response.parsed - if not isinstance(content, PostBodyMultipartResponse200): - raise AssertionError(f"Received status {response.status_code} from test server with payload: {content!r}") - - assert content.a_string == a_string - assert content.file_name == file_name - assert content.file_content_type == mime_type - assert content.file_data.encode() == payload - assert content.description == description + check_response(response) diff --git a/knope.toml b/knope.toml index 5ea5b3ddb..53b453bf4 100644 --- a/knope.toml +++ b/knope.toml @@ -1,6 +1,13 @@ [package] versioned_files = ["pyproject.toml"] changelog = "CHANGELOG.md" +extra_changelog_sections = [ + { types = ["security"], name = "🚨Security" }, + { types = ["major"], name = "❗️Breaking ❗" }, + { types = ["minor"], name = "🚀 Features" }, + { types = ["patch"], name = "🐛 Fixes" }, + { types = ["note"], name = "📝Notes" } +] [github] owner = "openapi-generators" diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index af6944ae4..dc7892ba8 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -4,19 +4,19 @@ import mimetypes import shutil import subprocess -from collections.abc import Sequence +from collections.abc import Callable, Sequence from importlib.metadata import version from pathlib import Path from subprocess import CalledProcessError -from typing import Any, Optional, Union +from typing import Any -import httpcore -import httpx +import httpcore2 +import httpx2 from jinja2 import BaseLoader, ChoiceLoader, Environment, FileSystemLoader, PackageLoader from ruamel.yaml import YAML from ruamel.yaml.error import YAMLError -from openapi_python_client import utils +from openapi_python_client import strings from .config import Config, MetaType from .parser import GeneratorData, import_string_from_class @@ -25,11 +25,16 @@ __version__ = version(__package__) +from .schema.untrusted_string import UntrustedString -TEMPLATE_FILTERS = { - "snakecase": utils.snake_case, - "kebabcase": utils.kebab_case, - "pascalcase": utils.pascal_case, +TEMPLATE_FILTERS: dict[str, Callable[..., object]] = { + "snakecase": strings.snake_case, + "kebabcase": strings.kebab_case, + "pascalcase": strings.pascal_case, + "safe_for_docstring": strings.safe_for_docstring, + "in_double_quote_literal": strings.in_double_quote_literal, + "in_f_string_literal": strings.in_f_string_literal, + "as_unembedded_code": strings.PythonCode.as_unembedded_code, "any": any, } @@ -42,7 +47,7 @@ def __init__( *, openapi: GeneratorData, config: Config, - custom_template_path: Optional[Path] = None, + custom_template_path: Path | None = None, ) -> None: self.openapi: GeneratorData = openapi self.config = config @@ -66,14 +71,14 @@ def __init__( keep_trailing_newline=True, ) - self.project_name: str = config.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client" + self.project_name: str = config.project_name_override or f"{strings.kebab_case(openapi.title).lower()}-client" self.package_name: str = config.package_name_override or self.project_name.replace("-", "_") self.project_dir: Path # Where the generated code will be placed self.package_dir: Path # Where the generated Python module will be placed (same as project_dir if no meta) if config.output_path is not None: self.project_dir = config.output_path - elif config.meta_type == MetaType.NONE: + elif config.meta_type == MetaType.NONE: # pragma: no cover self.project_dir = Path.cwd() / self.package_name else: self.project_dir = Path.cwd() / self.project_name @@ -83,16 +88,20 @@ def __init__( else: self.package_dir = self.project_dir / self.package_name - self.package_description: str = utils.remove_string_escapes( - f"A client library for accessing {self.openapi.title}" + self.package_description = UntrustedString( + f"A client library for accessing {self.openapi.title.get_untrusted_value()}" + ) + self.version: UntrustedString = ( + UntrustedString(config.package_version_override) if config.package_version_override else openapi.version ) - self.version: str = config.package_version_override or openapi.version self.env.filters.update(TEMPLATE_FILTERS) self.env.globals.update( - utils=utils, - python_identifier=lambda x: utils.PythonIdentifier(x, config.field_prefix), - class_name=lambda x: utils.ClassName(x, config.field_prefix), + config=config, + strings=strings, + PythonCode=strings.PythonCode, + python_identifier=lambda x: strings.PythonIdentifier(x, config.field_prefix), + class_name=lambda x: strings.ClassName(x, config.field_prefix), package_name=self.package_name, package_dir=self.package_dir, package_description=self.package_description, @@ -109,7 +118,7 @@ def build(self) -> Sequence[GeneratorError]: print(f"Generating {self.project_dir}") try: - self.project_dir.mkdir() + self.project_dir.mkdir(parents=True) except FileExistsError: if not self.config.overwrite: return [GeneratorError(detail="Directory already exists. Delete it or use the --overwrite option.")] @@ -125,7 +134,7 @@ def _run_post_hooks(self) -> None: self._run_command(command) def _run_command(self, cmd: str) -> None: - cmd_name = cmd.split(" ")[0] + cmd_name = cmd.split(" ", maxsplit=1)[0] command_exists = shutil.which(cmd_name) if not command_exists: self.errors.append( @@ -183,7 +192,7 @@ def _build_metadata(self) -> None: readme = self.project_dir / "README.md" readme_template = self.env.get_template("README.md.jinja") readme.write_text( - readme_template.render(poetry=self.config.meta_type == MetaType.POETRY), + readme_template.render(meta=self.config.meta_type), encoding=self.config.file_encoding, ) @@ -266,7 +275,8 @@ def _build_api(self) -> None: endpoint_collections_by_tag = self.openapi.endpoint_collections_by_tag endpoint_template = self.env.get_template( - "endpoint_module.py.jinja", globals={"isbool": lambda obj: obj.get_base_type_string() == "bool"} + "endpoint_module.py.jinja", + globals={"isbool": lambda obj: obj.get_base_type_string() == strings.PythonCode("bool")}, ) for tag, collection in endpoint_collections_by_tag.items(): tag_dir = api_dir / tag @@ -280,7 +290,7 @@ def _build_api(self) -> None: ) for endpoint in collection.endpoints: - module_path = tag_dir / f"{utils.PythonIdentifier(endpoint.name, self.config.field_prefix)}.py" + module_path = tag_dir / f"{strings.PythonIdentifier(endpoint.name, self.config.field_prefix)}.py" module_path.write_text( endpoint_template.render( endpoint=endpoint, @@ -291,8 +301,8 @@ def _build_api(self) -> None: def _get_project_for_url_or_path( config: Config, - custom_template_path: Optional[Path] = None, -) -> Union[Project, GeneratorError]: + custom_template_path: Path | None = None, +) -> Project | GeneratorError: data_dict = _get_document(source=config.document_source, timeout=config.http_timeout) if isinstance(data_dict, GeneratorError): return data_dict @@ -309,7 +319,7 @@ def _get_project_for_url_or_path( def generate( *, config: Config, - custom_template_path: Optional[Path] = None, + custom_template_path: Path | None = None, ) -> Sequence[GeneratorError]: """ Generate the client library @@ -326,7 +336,7 @@ def generate( return project.build() -def _load_yaml_or_json(data: bytes, content_type: Optional[str]) -> Union[dict[str, Any], GeneratorError]: +def _load_yaml_or_json(data: bytes, content_type: str | None) -> dict[str, Any] | GeneratorError: if content_type == "application/json": try: return json.loads(data.decode()) @@ -340,19 +350,19 @@ def _load_yaml_or_json(data: bytes, content_type: Optional[str]) -> Union[dict[s return GeneratorError(header=f"Invalid YAML from provided source: {err}") -def _get_document(*, source: Union[str, Path], timeout: int) -> Union[dict[str, Any], GeneratorError]: +def _get_document(*, source: str | Path, timeout: int) -> dict[str, Any] | GeneratorError: yaml_bytes: bytes - content_type: Optional[str] + content_type: str | None if isinstance(source, str): try: - response = httpx.get(source, timeout=timeout) + response = httpx2.get(source, timeout=timeout) yaml_bytes = response.content if "content-type" in response.headers: content_type = response.headers["content-type"].split(";")[0] else: # pragma: no cover content_type = mimetypes.guess_type(source, strict=True)[0] - except (httpx.HTTPError, httpcore.NetworkError): + except (httpx2.HTTPError, httpcore2.NetworkError): return GeneratorError(header="Could not get OpenAPI document from provided URL") else: yaml_bytes = source.read_bytes() diff --git a/openapi_python_client/cli.py b/openapi_python_client/cli.py index 92d91f943..22133c38d 100644 --- a/openapi_python_client/cli.py +++ b/openapi_python_client/cli.py @@ -1,12 +1,10 @@ import codecs from collections.abc import Sequence from pathlib import Path -from pprint import pformat -from typing import Optional, Union import typer -from openapi_python_client import MetaType +from openapi_python_client import MetaType, __version__ from openapi_python_client.config import Config, ConfigFile from openapi_python_client.parser.errors import ErrorLevel, GeneratorError, ParseError @@ -14,8 +12,6 @@ def _version_callback(value: bool) -> None: - from openapi_python_client import __version__ - if value: typer.echo(f"openapi-python-client version: {__version__}") raise typer.Exit() @@ -23,15 +19,15 @@ def _version_callback(value: bool) -> None: def _process_config( *, - url: Optional[str], - path: Optional[Path], - config_path: Optional[Path], + url: str | None, + path: Path | None, + config_path: Path | None, meta_type: MetaType, file_encoding: str, overwrite: bool, - output_path: Optional[Path], + output_path: Path | None, ) -> Config: - source: Union[Path, str] + source: Path | str if url and not path: source = url elif path and not url: @@ -57,7 +53,14 @@ def _process_config( except Exception as err: raise typer.BadParameter("Unable to parse config") from err - return Config.from_sources(config_file, meta_type, source, file_encoding, overwrite, output_path=output_path) + return Config.from_sources( + config_file=config_file, + meta_type=meta_type, + document_source=source, + file_encoding=file_encoding, + overwrite=overwrite, + output_path=output_path, + ) # noinspection PyUnusedLocal @@ -72,16 +75,16 @@ def cli( def _print_parser_error(err: GeneratorError, color: str) -> None: typer.secho(err.header, bold=True, fg=color, err=True) - typer.echo() + typer.echo(err=True) if err.detail: typer.secho(err.detail, fg=color, err=True) - typer.echo() + typer.echo(err=True) if isinstance(err, ParseError) and err.data is not None: - formatted_data = pformat(err.data) + formatted_data = err.data.model_dump_json(indent=2, exclude_unset=True, by_alias=True) typer.secho(formatted_data, fg=color, err=True) - typer.echo() + typer.echo(err=True) def handle_errors(errors: Sequence[GeneratorError], fail_on_warning: bool = False) -> None: @@ -127,9 +130,10 @@ def handle_errors(errors: Sequence[GeneratorError], fail_on_warning: bool = Fals @app.command() def generate( - url: Optional[str] = typer.Option(None, help="A URL to read the OpenAPI document from"), - path: Optional[Path] = typer.Option(None, help="A path to the OpenAPI document"), - custom_template_path: Optional[Path] = typer.Option( + *, + url: str | None = typer.Option(None, help="A URL to read the OpenAPI document from"), + path: Path | None = typer.Option(None, help="A path to the OpenAPI document"), + custom_template_path: Path | None = typer.Option( None, help="A path to a directory containing custom template(s)", file_okay=False, @@ -142,10 +146,10 @@ def generate( help="The type of metadata you want to generate.", ), file_encoding: str = typer.Option("utf-8", help="Encoding used when writing generated"), - config_path: Optional[Path] = typer.Option(None, "--config", help="Path to the config file to use"), + config_path: Path | None = typer.Option(None, "--config", help="Path to the config file to use"), fail_on_warning: bool = False, overwrite: bool = typer.Option(False, help="Overwrite the existing client if it exists"), - output_path: Optional[Path] = typer.Option( + output_path: Path | None = typer.Option( None, help="Path to write the generated code to. " "Defaults to the OpenAPI document title converted to kebab or snake case (depending on meta type). " @@ -153,7 +157,7 @@ def generate( ), ) -> None: """Generate a new OpenAPI Client library""" - from . import generate + from . import generate # noqa: PLC0415 config = _process_config( url=url, diff --git a/openapi_python_client/config.py b/openapi_python_client/config.py index 9cc002d12..dbf03349d 100644 --- a/openapi_python_client/config.py +++ b/openapi_python_client/config.py @@ -1,13 +1,14 @@ import json import mimetypes -from enum import Enum +from enum import StrEnum from pathlib import Path -from typing import Optional, Union from attr import define from pydantic import BaseModel from ruamel.yaml import YAML +from openapi_python_client.schema.untrusted_string import UntrustedString + class ClassOverride(BaseModel): """An override of a single generated class. @@ -15,17 +16,18 @@ class ClassOverride(BaseModel): See https://github.com/openapi-generators/openapi-python-client#class_overrides """ - class_name: Optional[str] = None - module_name: Optional[str] = None + class_name: UntrustedString | None = None + module_name: str | None = None -class MetaType(str, Enum): +class MetaType(StrEnum): """The types of metadata supported for project generation.""" NONE = "none" POETRY = "poetry" SETUP = "setup" PDM = "pdm" + UV = "uv" class ConfigFile(BaseModel): @@ -34,13 +36,14 @@ class ConfigFile(BaseModel): See https://github.com/openapi-generators/openapi-python-client#configuration """ - class_overrides: Optional[dict[str, ClassOverride]] = None - content_type_overrides: Optional[dict[str, str]] = None - project_name_override: Optional[str] = None - package_name_override: Optional[str] = None - package_version_override: Optional[str] = None + class_overrides: dict[str, ClassOverride] | None = None + content_type_overrides: dict[str, str] | None = None + project_name_override: str | None = None + package_name_override: str | None = None + package_version_override: str | None = None use_path_prefixes_for_title_model_names: bool = True - post_hooks: Optional[list[str]] = None + post_hooks: list[str] | None = None + docstrings_on_attributes: bool = False field_prefix: str = "field_" generate_all_tags: bool = False http_timeout: int = 5 @@ -65,40 +68,42 @@ class Config: meta_type: MetaType class_overrides: dict[str, ClassOverride] - project_name_override: Optional[str] - package_name_override: Optional[str] - package_version_override: Optional[str] + project_name_override: str | None + package_name_override: str | None + package_version_override: str | None use_path_prefixes_for_title_model_names: bool post_hooks: list[str] + docstrings_on_attributes: bool field_prefix: str generate_all_tags: bool http_timeout: int literal_enums: bool - document_source: Union[Path, str] + document_source: Path | str file_encoding: str content_type_overrides: dict[str, str] overwrite: bool - output_path: Optional[Path] + output_path: Path | None @staticmethod def from_sources( + *, config_file: ConfigFile, meta_type: MetaType, - document_source: Union[Path, str], + document_source: Path | str, file_encoding: str, overwrite: bool, - output_path: Optional[Path], + output_path: Path | None, ) -> "Config": if config_file.post_hooks is not None: post_hooks = config_file.post_hooks elif meta_type == MetaType.NONE: post_hooks = [ - "ruff check . --fix --extend-select=I", + "ruff check . --fix-only --extend-select=I", "ruff format .", ] else: post_hooks = [ - "ruff check --fix .", + "ruff check --fix-only .", "ruff format .", ] @@ -111,6 +116,7 @@ def from_sources( package_version_override=config_file.package_version_override, use_path_prefixes_for_title_model_names=config_file.use_path_prefixes_for_title_model_names, post_hooks=post_hooks, + docstrings_on_attributes=config_file.docstrings_on_attributes, field_prefix=config_file.field_prefix, generate_all_tags=config_file.generate_all_tags, http_timeout=config_file.http_timeout, diff --git a/openapi_python_client/parser/bodies.py b/openapi_python_client/parser/bodies.py index c51966412..ae6e30fd9 100644 --- a/openapi_python_client/parser/bodies.py +++ b/openapi_python_client/parser/bodies.py @@ -1,5 +1,4 @@ -import sys -from typing import Union +from enum import StrEnum import attr @@ -9,33 +8,25 @@ Schemas, property_from_data, ) +from openapi_python_client.parser.properties.schemas import get_reference_simple_name from .. import schema as oai from ..config import Config -from ..utils import get_content_type +from ..schema.untrusted_string import UntrustedString +from ..strings import get_content_type from .errors import ErrorLevel, ParseError -if sys.version_info >= (3, 11): - from enum import StrEnum - class BodyType(StrEnum): - JSON = "json" - DATA = "data" - FILES = "files" - CONTENT = "content" -else: - from enum import Enum - - class BodyType(str, Enum): - JSON = "json" - DATA = "data" - FILES = "files" - CONTENT = "content" +class BodyType(StrEnum): + JSON = "json" + DATA = "data" + FILES = "files" + CONTENT = "content" @attr.define class Body: - content_type: str + content_type: UntrustedString prop: Property body_type: BodyType @@ -44,10 +35,10 @@ def body_from_data( *, data: oai.Operation, schemas: Schemas, - request_bodies: dict[str, Union[oai.RequestBody, oai.Reference]], + request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference], config: Config, endpoint_name: str, -) -> tuple[list[Union[Body, ParseError]], Schemas]: +) -> tuple[list[Body | ParseError], Schemas]: """Adds form or JSON body to Endpoint if included in data""" body = _resolve_reference(data.request_body, request_bodies) if isinstance(body, ParseError): @@ -55,12 +46,12 @@ def body_from_data( if body is None: return [], schemas - bodies: list[Union[Body, ParseError]] = [] + bodies: list[Body | ParseError] = [] body_content = body.content prefix_type_names = len(body_content) > 1 for content_type, media_type in body_content.items(): - simplified_content_type = get_content_type(content_type, config) + simplified_content_type = get_content_type(content_type.get_untrusted_value(), config) if simplified_content_type is None: bodies.append( ParseError( @@ -98,8 +89,8 @@ def body_from_data( ) continue prop, schemas = property_from_data( - name="body", - required=True, + name=UntrustedString("body"), + required=body.required, data=media_type_schema, schemas=schemas, parent_name=f"{endpoint_name}_{body_type}" if prefix_type_names else endpoint_name, @@ -131,16 +122,16 @@ def body_from_data( def _resolve_reference( - body: Union[oai.RequestBody, oai.Reference, None], request_bodies: dict[str, Union[oai.RequestBody, oai.Reference]] -) -> Union[oai.RequestBody, ParseError, None]: + body: oai.RequestBody | oai.Reference | None, request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference] +) -> oai.RequestBody | ParseError | None: if body is None: return None references_seen = [] while isinstance(body, oai.Reference) and body.ref not in references_seen: references_seen.append(body.ref) - body = request_bodies.get(body.ref.split("/")[-1]) + body = request_bodies.get(get_reference_simple_name(body.ref)) if isinstance(body, oai.Reference): return ParseError(detail="Circular $ref in request body", data=body) if body is None and references_seen: - return ParseError(detail=f"Could not resolve $ref {references_seen[-1]} in request body") + return ParseError(detail=f"Could not resolve $ref {references_seen[-1].get_untrusted_value()} in request body") return body diff --git a/openapi_python_client/parser/errors.py b/openapi_python_client/parser/errors.py index 36322f0cf..00f522b9e 100644 --- a/openapi_python_client/parser/errors.py +++ b/openapi_python_client/parser/errors.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from enum import Enum -from typing import Optional __all__ = ["ErrorLevel", "GeneratorError", "ParameterError", "ParseError", "PropertyError"] @@ -18,7 +17,7 @@ class ErrorLevel(Enum): class GeneratorError: """Base data struct containing info on an error that occurred""" - detail: Optional[str] = None + detail: str | None = None level: ErrorLevel = ErrorLevel.ERROR header: str = "Unable to generate the client" @@ -28,7 +27,7 @@ class ParseError(GeneratorError): """An error raised when there's a problem parsing an OpenAPI document""" level: ErrorLevel = ErrorLevel.WARNING - data: Optional[BaseModel] = None + data: BaseModel | None = None header: str = "Unable to parse this part of your OpenAPI document: " diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 117b2ee30..c16f03f2f 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -2,15 +2,15 @@ from collections.abc import Iterator from copy import deepcopy from dataclasses import dataclass, field -from http import HTTPStatus -from typing import Any, Optional, Protocol, Union +from typing import Any, Protocol from pydantic import ValidationError from .. import schema as oai -from .. import utils +from .. import strings from ..config import Config -from ..utils import PythonIdentifier +from ..schema.untrusted_string import UntrustedString +from ..strings import PythonIdentifier from .bodies import Body, body_from_data from .errors import GeneratorError, ParseError, PropertyError from .properties import ( @@ -26,7 +26,7 @@ property_from_data, ) from .properties.schemas import parameter_from_reference -from .responses import Response, response_from_data +from .responses import HTTPStatusPattern, Responses, response_from_data _PATH_PARAM_REGEX = re.compile("{([a-zA-Z_-][a-zA-Z0-9_-]*)}") @@ -47,24 +47,28 @@ class EndpointCollection: @staticmethod def from_data( *, - data: dict[str, oai.PathItem], + data: dict[UntrustedString, oai.PathItem], schemas: Schemas, parameters: Parameters, - request_bodies: dict[str, Union[oai.RequestBody, oai.Reference]], + request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference], + responses: dict[UntrustedString, oai.Response | oai.Reference], config: Config, - ) -> tuple[dict[utils.PythonIdentifier, "EndpointCollection"], Schemas, Parameters]: + ) -> tuple[dict[strings.PythonIdentifier, "EndpointCollection"], Schemas, Parameters]: """Parse the openapi paths data to get EndpointCollections by tag""" - endpoints_by_tag: dict[utils.PythonIdentifier, EndpointCollection] = {} + endpoints_by_tag: dict[strings.PythonIdentifier, EndpointCollection] = {} methods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"] for path, path_data in data.items(): for method in methods: - operation: Optional[oai.Operation] = getattr(path_data, method) + operation: oai.Operation | None = getattr(path_data, method) if operation is None: continue - tags = [utils.PythonIdentifier(value=tag, prefix="tag") for tag in operation.tags or ["default"]] + tags = [ + strings.PythonIdentifier(value=tag, prefix="tag") + for tag in operation.tags or [UntrustedString("default")] + ] if not config.generate_all_tags: tags = tags[:1] @@ -78,6 +82,7 @@ def from_data( schemas=schemas, parameters=parameters, request_bodies=request_bodies, + responses=responses, config=config, ) # Add `PathItem` parameters @@ -92,12 +97,14 @@ def from_data( if not isinstance(endpoint, ParseError): endpoint = Endpoint.sort_parameters(endpoint=endpoint) if isinstance(endpoint, ParseError): - endpoint.header = f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}. Endpoint will not be generated." + endpoint.header = f"WARNING parsing {method.upper()} {path.get_untrusted_value()} within {'/'.join(tags)}. Endpoint will not be generated." for collection in collections: collection.parse_errors.append(endpoint) continue for error in endpoint.errors: - error.header = f"WARNING parsing {method.upper()} {path} within {'/'.join(tags)}." + error.header = ( + f"WARNING parsing {method.upper()} {path.get_untrusted_value()} within {'/'.join(tags)}." + ) for collection in collections: collection.parse_errors.append(error) for collection in collections: @@ -106,14 +113,14 @@ def from_data( return endpoints_by_tag, schemas, parameters -def generate_operation_id(*, path: str, method: str) -> str: +def generate_operation_id(*, path: UntrustedString, method: str, prefix: str) -> PythonIdentifier: """Generate an operationId from a path""" - clean_path = path.replace("{", "").replace("}", "").replace("/", "_") - if clean_path.startswith("_"): - clean_path = clean_path[1:] - if clean_path.endswith("_"): - clean_path = clean_path[:-1] - return f"{method}_{clean_path}" + unsafe_id = path.get_untrusted_value().replace("{", "").replace("}", "").replace("/", "_") + if unsafe_id.startswith("_"): + unsafe_id = unsafe_id[1:] + if unsafe_id.endswith("_"): # pragma: no cover + unsafe_id = unsafe_id[:-1] + return PythonIdentifier(f"{method}_{unsafe_id}", prefix=prefix) models_relative_prefix: str = "..." @@ -124,7 +131,7 @@ class RequestBodyParser(Protocol): def __call__( self, *, body: oai.RequestBody, schemas: Schemas, parent_name: str, config: Config - ) -> tuple[Union[Property, PropertyError, None], Schemas]: ... # pragma: no cover + ) -> tuple[Property | PropertyError | None, Schemas]: ... # pragma: no cover @dataclass @@ -133,47 +140,43 @@ class Endpoint: Describes a single endpoint on the server """ - path: str + path: UntrustedString method: str - description: Optional[str] - name: str + description: UntrustedString + name: PythonIdentifier requires_security: bool tags: list[PythonIdentifier] - summary: Optional[str] = "" + summary: UntrustedString relative_imports: set[str] = field(default_factory=set) query_parameters: list[Property] = field(default_factory=list) path_parameters: list[Property] = field(default_factory=list) header_parameters: list[Property] = field(default_factory=list) cookie_parameters: list[Property] = field(default_factory=list) - responses: list[Response] = field(default_factory=list) + responses: Responses = field(default_factory=lambda: Responses(patterns=[], default=None)) bodies: list[Body] = field(default_factory=list) errors: list[ParseError] = field(default_factory=list) @staticmethod def _add_responses( - *, endpoint: "Endpoint", data: oai.Responses, schemas: Schemas, config: Config + *, + endpoint: "Endpoint", + data: oai.Responses, + schemas: Schemas, + responses: dict[UntrustedString, oai.Response | oai.Reference], + config: Config, ) -> tuple["Endpoint", Schemas]: endpoint = deepcopy(endpoint) for code, response_data in data.items(): - status_code: HTTPStatus - try: - status_code = HTTPStatus(int(code)) - except ValueError: - endpoint.errors.append( - ParseError( - detail=( - f"Invalid response status code {code} (not a valid HTTP " - f"status code), response will be omitted from generated " - f"client" - ) - ) - ) + status_code = HTTPStatusPattern.parse(code) + if isinstance(status_code, ParseError): + endpoint.errors.append(status_code) continue response, schemas = response_from_data( status_code=status_code, data=response_data, schemas=schemas, + responses=responses, parent_name=endpoint.name, config=config, ) @@ -182,7 +185,7 @@ def _add_responses( endpoint.errors.append( ParseError( detail=( - f"Cannot parse response for status code {status_code}{detail_suffix}, " + f"Cannot parse response for status code {code}{detail_suffix}, " f"response will be omitted from generated client" ), data=response.data, @@ -193,18 +196,22 @@ def _add_responses( # No reasons to use lazy imports in endpoints, so add lazy imports to relative here. endpoint.relative_imports |= response.prop.get_lazy_imports(prefix=models_relative_prefix) endpoint.relative_imports |= response.prop.get_imports(prefix=models_relative_prefix) - endpoint.responses.append(response) + if response.is_default(): + endpoint.responses.default = response + else: + endpoint.responses.patterns.append(response) + endpoint.responses.patterns.sort() return endpoint, schemas @staticmethod def add_parameters( *, endpoint: "Endpoint", - data: Union[oai.Operation, oai.PathItem], + data: oai.Operation | oai.PathItem, schemas: Schemas, parameters: Parameters, config: Config, - ) -> tuple[Union["Endpoint", ParseError], Schemas, Parameters]: + ) -> tuple["Endpoint | ParseError", Schemas, Parameters]: """Process the defined `parameters` for an Endpoint. Any existing parameters will be ignored, so earlier instances of a parameter take precedence. PathItem @@ -233,7 +240,7 @@ def add_parameters( endpoint = deepcopy(endpoint) - unique_parameters: set[tuple[str, oai.ParameterLocation]] = set() + unique_parameters: set[tuple[UntrustedString, oai.ParameterLocation]] = set() parameters_by_location: dict[str, list[Property]] = { oai.ParameterLocation.QUERY: endpoint.query_parameters, oai.ParameterLocation.PATH: endpoint.path_parameters, @@ -259,7 +266,7 @@ def add_parameters( detail=( "Parameters MUST NOT contain duplicates. " "A unique parameter is defined by a combination of a name and location. " - f"Duplicated parameters named `{param.name}` detected in `{param.param_in}`." + f"Duplicated parameters named `{param.name.get_untrusted_value()}` detected in `{param.param_in}`." ), ), schemas, @@ -311,8 +318,8 @@ def _check_parameters_for_conflicts( self, *, config: Config, - previously_modified_params: Optional[set[tuple[oai.ParameterLocation, str]]] = None, - ) -> Union["Endpoint", ParseError]: + previously_modified_params: set[tuple[oai.ParameterLocation, UntrustedString]] | None = None, + ) -> "Endpoint | ParseError": """Check for conflicting parameters For parameters that have the same python_name but are in different locations, append the location to the @@ -329,7 +336,7 @@ def _check_parameters_for_conflicts( location, prop = parameter if prop.python_name in reserved_names: - prop.set_python_name(new_name=f"{prop.python_name}_{location}", config=config) + prop.set_python_name(new_name=UntrustedString(f"{prop.python_name}_{location}"), config=config) modified_params.add((location, prop.name)) continue @@ -348,9 +355,9 @@ def _check_parameters_for_conflicts( if location != conflicting_location: conflicting_prop.set_python_name( - new_name=f"{conflicting_prop.python_name}_{conflicting_location}", config=config + new_name=UntrustedString(f"{conflicting_prop.python_name}_{conflicting_location}"), config=config ) - prop.set_python_name(new_name=f"{prop.python_name}_{location}", config=config) + prop.set_python_name(new_name=UntrustedString(f"{prop.python_name}_{location}"), config=config) elif conflicting_prop.name != prop.name: # Use the name to differentiate conflicting_prop.set_python_name(new_name=conflicting_prop.name, config=config, skip_snake_case=True) prop.set_python_name(new_name=prop.name, config=config, skip_snake_case=True) @@ -365,7 +372,7 @@ def _check_parameters_for_conflicts( return self @staticmethod - def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]: + def sort_parameters(*, endpoint: "Endpoint") -> "Endpoint | ParseError": """ Sorts the path parameters of an `endpoint` so that they match the order declared in `endpoint.path`. @@ -377,7 +384,7 @@ def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]: the path parameters and they could not be sorted. """ endpoint = deepcopy(endpoint) - parameters_from_path = re.findall(_PATH_PARAM_REGEX, endpoint.path) + parameters_from_path = re.findall(_PATH_PARAM_REGEX, endpoint.path.get_untrusted_value()) try: endpoint.path_parameters.sort( key=lambda param: parameters_from_path.index(param.name), @@ -387,36 +394,41 @@ def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]: if parameters_from_path != [param.name for param in endpoint.path_parameters]: return ParseError( - detail=f"Incorrect path templating for {endpoint.path} (Path parameters do not match with path)", + detail=f"Incorrect path templating for {endpoint.path.get_untrusted_value()} (Path parameters do not match with path)", ) for parameter in endpoint.path_parameters: - endpoint.path = endpoint.path.replace(f"{{{parameter.name}}}", f"{{{parameter.python_name}}}") + endpoint.path = UntrustedString( + endpoint.path.get_untrusted_value().replace( + f"{{{parameter.name.get_untrusted_value()}}}", f"{{{parameter.python_name}}}" + ) + ) return endpoint @staticmethod def from_data( *, data: oai.Operation, - path: str, + path: UntrustedString, method: str, tags: list[PythonIdentifier], schemas: Schemas, parameters: Parameters, - request_bodies: dict[str, Union[oai.RequestBody, oai.Reference]], + request_bodies: dict[UntrustedString, oai.RequestBody | oai.Reference], + responses: dict[UntrustedString, oai.Response | oai.Reference], config: Config, - ) -> tuple[Union["Endpoint", ParseError], Schemas, Parameters]: + ) -> tuple["Endpoint | ParseError", Schemas, Parameters]: """Construct an endpoint from the OpenAPI data""" if data.operationId is None: - name = generate_operation_id(path=path, method=method) + name = generate_operation_id(path=path, method=method, prefix=config.field_prefix) else: - name = data.operationId + name = PythonIdentifier(data.operationId, prefix=config.field_prefix) endpoint = Endpoint( path=path, method=method, - summary=utils.remove_string_escapes(data.summary) if data.summary else "", - description=utils.remove_string_escapes(data.description) if data.description else "", + summary=data.summary if data.summary else UntrustedString(""), + description=data.description if data.description else UntrustedString(""), name=name, requires_security=bool(data.security), tags=tags, @@ -431,8 +443,14 @@ def from_data( ) if isinstance(result, ParseError): return result, schemas, parameters - result, schemas = Endpoint._add_responses(endpoint=result, data=data.responses, schemas=schemas, config=config) - if isinstance(result, ParseError): + result, schemas = Endpoint._add_responses( + endpoint=result, + data=data.responses, + schemas=schemas, + responses=responses, + config=config, + ) + if isinstance(result, ParseError): # pragma: no cover return result, schemas, parameters bodies, schemas = body_from_data( data=data, schemas=schemas, config=config, endpoint_name=result.name, request_bodies=request_bodies @@ -459,14 +477,14 @@ def from_data( return result, schemas, parameters - def response_type(self) -> str: + def response_type(self) -> strings.PythonCode: """Get the Python type of any response from this endpoint""" - types = sorted({response.prop.get_type_string(quoted=False) for response in self.responses}) + types = sorted({response.prop.get_type_string().as_unembedded_code() for response in self.responses}) if len(types) == 0: - return "Any" + return strings.PythonCode("Any") if len(types) == 1: - return self.responses[0].prop.get_type_string(quoted=False) - return f"Union[{', '.join(types)}]" + return strings.PythonCode(types[0]) + return strings.PythonCode(" | ".join(types)) def iter_all_parameters(self) -> Iterator[tuple[oai.ParameterLocation, Property]]: """Iterate through all the parameters of this endpoint""" @@ -490,16 +508,16 @@ def list_all_parameters(self) -> list[Property]: class GeneratorData: """All the data needed to generate a client""" - title: str - description: Optional[str] - version: str - models: Iterator[ModelProperty] + title: UntrustedString + description: UntrustedString | None + version: UntrustedString + models: list[ModelProperty] errors: list[ParseError] - endpoint_collections_by_tag: dict[utils.PythonIdentifier, EndpointCollection] - enums: Iterator[Union[EnumProperty, LiteralEnumProperty]] + endpoint_collections_by_tag: dict[strings.PythonIdentifier, EndpointCollection] + enums: list[EnumProperty | LiteralEnumProperty] @staticmethod - def from_dict(data: dict[str, Any], *, config: Config) -> Union["GeneratorData", GeneratorError]: + def from_dict(data: dict[str, Any], *, config: Config) -> "GeneratorData | GeneratorError": """Create an OpenAPI from dict""" try: openapi = oai.OpenAPI.model_validate(data) @@ -521,14 +539,20 @@ def from_dict(data: dict[str, Any], *, config: Config) -> Union["GeneratorData", config=config, ) request_bodies = (openapi.components and openapi.components.requestBodies) or {} + responses = (openapi.components and openapi.components.responses) or {} endpoint_collections_by_tag, schemas, parameters = EndpointCollection.from_data( - data=openapi.paths, schemas=schemas, parameters=parameters, request_bodies=request_bodies, config=config + data=openapi.paths, + schemas=schemas, + parameters=parameters, + request_bodies=request_bodies, + responses=responses, + config=config, ) - enums = ( - prop for prop in schemas.classes_by_name.values() if isinstance(prop, (EnumProperty, LiteralEnumProperty)) - ) - models = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, ModelProperty)) + enums = [ + prop for prop in schemas.classes_by_name.values() if isinstance(prop, EnumProperty | LiteralEnumProperty) + ] + models = [prop for prop in schemas.classes_by_name.values() if isinstance(prop, ModelProperty)] return GeneratorData( title=openapi.info.title, diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index ba667347b..9a60c832d 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -18,8 +18,10 @@ from attrs import evolve -from ... import Config, utils +from ... import Config, strings from ... import schema as oai +from ...schema.ref import Ref +from ...schema.untrusted_string import UntrustedString from ..errors import ParameterError, ParseError, PropertyError from .any import AnyProperty from .boolean import BooleanProperty @@ -40,6 +42,7 @@ Parameters, ReferencePath, Schemas, + get_reference_simple_name, parse_reference_path, update_parameters_with_data, update_schemas_with_data, @@ -50,11 +53,11 @@ def _string_based_property( - name: str, required: bool, data: oai.Schema, config: Config + name: UntrustedString, required: bool, data: oai.Schema, config: Config ) -> StringProperty | DateProperty | DateTimeProperty | FileProperty | UuidProperty | PropertyError: """Construct a Property from the type "string" """ string_format = data.schema_format - python_name = utils.PythonIdentifier(value=name, prefix=config.field_prefix) + python_name = strings.PythonIdentifier(value=name, prefix=config.field_prefix) if string_format == "date-time": return DateTimeProperty.build( name=name, @@ -102,16 +105,17 @@ def _string_based_property( def _property_from_ref( - name: str, + *, + name: UntrustedString, required: bool, parent: oai.Schema | None, data: oai.Reference, schemas: Schemas, config: Config, - roots: set[ReferencePath | utils.ClassName], + roots: set[ReferencePath | strings.ClassName], ) -> tuple[Property | PropertyError, Schemas]: ref_path = parse_reference_path(data.ref) - if isinstance(ref_path, ParseError): + if isinstance(ref_path, ParseError): # pragma: no cover return PropertyError(data=data, detail=ref_path.detail), schemas existing = schemas.classes_by_reference.get(ref_path) if not existing: @@ -129,7 +133,7 @@ def _property_from_ref( existing, required=required, name=name, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), default=default, # type: ignore # mypy can't tell that default comes from the same class... ) @@ -138,18 +142,18 @@ def _property_from_ref( def property_from_data( # noqa: PLR0911, PLR0912 - name: str, + *, + name: UntrustedString, required: bool, data: oai.Reference | oai.Schema, schemas: Schemas, parent_name: str, config: Config, process_properties: bool = True, - roots: set[ReferencePath | utils.ClassName] | None = None, + roots: set[ReferencePath | strings.ClassName] | None = None, ) -> tuple[Property | PropertyError, Schemas]: """Generate a Property from the OpenAPI dictionary representation of it""" roots = roots or set() - name = utils.remove_string_escapes(name) if isinstance(data, oai.Reference): return _property_from_ref( name=name, @@ -189,7 +193,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 name=name, required=required, default=data.default, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), @@ -229,7 +233,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 required=required, default=data.default, const=data.const, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, ), schemas, @@ -245,7 +249,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 name=name, default=data.default, required=required, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), @@ -257,7 +261,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 name=name, default=data.default, required=required, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), @@ -269,7 +273,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 name=name, required=required, default=None, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), @@ -302,7 +306,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 name=name, required=required, default=data.default, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), @@ -312,11 +316,11 @@ def property_from_data( # noqa: PLR0911, PLR0912 def _create_schemas( *, - components: dict[str, oai.Reference | oai.Schema], + components: dict[UntrustedString, oai.Reference | oai.Schema], schemas: Schemas, config: Config, ) -> Schemas: - to_process: Iterable[tuple[str, oai.Reference | oai.Schema]] = components.items() + to_process: Iterable[tuple[UntrustedString, oai.Reference | oai.Schema]] = components.items() still_making_progress = True errors: list[PropertyError] = [] @@ -324,17 +328,30 @@ def _create_schemas( while still_making_progress: still_making_progress = False errors = [] - next_round = [] + next_round: list[tuple[UntrustedString, oai.Reference | oai.Schema]] = [] # Only accumulate errors from the last round, since we might fix some along the way for name, data in to_process: - if isinstance(data, oai.Reference): - schemas.errors.append(PropertyError(data=data, detail="Reference schemas are not supported.")) - continue - ref_path = parse_reference_path(f"#/components/schemas/{name}") - if isinstance(ref_path, ParseError): + schema_data: oai.Reference | oai.Schema | None = data + ref_path = parse_reference_path(Ref(f"#/components/schemas/{name.get_untrusted_value()}")) + if isinstance(ref_path, ParseError): # pragma: no cover schemas.errors.append(PropertyError(detail=ref_path.detail, data=data)) continue - schemas_or_err = update_schemas_with_data(ref_path=ref_path, data=data, schemas=schemas, config=config) + if isinstance(data, oai.Reference): + # Fully dereference reference schemas + seen = [name] + while isinstance(schema_data, oai.Reference): + data_ref_schema = get_reference_simple_name(schema_data.ref) + if data_ref_schema in seen: + schemas.errors.append(PropertyError(detail="Circular schema references found", data=data)) + break + # use derefenced schema definition for this schema + schema_data = components.get(data_ref_schema) + if isinstance(schema_data, oai.Schema): + schemas_or_err = update_schemas_with_data( + ref_path=ref_path, data=schema_data, schemas=schemas, config=config + ) + else: + schemas.errors.append(PropertyError(detail="Referent schema not found", data=data)) if isinstance(schemas_or_err, PropertyError): next_round.append((name, data)) errors.append(schemas_or_err) @@ -347,13 +364,13 @@ def _create_schemas( return schemas -def _propogate_removal(*, root: ReferencePath | utils.ClassName, schemas: Schemas, error: PropertyError) -> None: - if isinstance(root, utils.ClassName): +def _propogate_removal(*, root: ReferencePath | strings.ClassName, schemas: Schemas, error: PropertyError) -> None: + if isinstance(root, strings.ClassName): schemas.classes_by_name.pop(root, None) return if root in schemas.classes_by_reference: error.detail = error.detail or "" - error.detail += f"\n{root}" + error.detail += f"\n{root.get_untrusted_value()}" del schemas.classes_by_reference[root] for child in schemas.dependencies.get(root, set()): _propogate_removal(root=child, schemas=schemas, error=error) @@ -385,10 +402,10 @@ def _process_models(*, schemas: Schemas, config: Config) -> Schemas: for model_prop in to_process: schemas_or_err = process_model(model_prop, schemas=schemas, config=config) if isinstance(schemas_or_err, PropertyError): - schemas_or_err.header = f"\nUnable to process schema {model_prop.name}:" + schemas_or_err.header = f"\nUnable to process schema {model_prop.name.get_untrusted_value()}:" if isinstance(schemas_or_err.data, oai.Reference) and schemas_or_err.data.ref.endswith( f"/{model_prop.class_info.name}" - ): + ): # pragma: no cover schemas_or_err.detail = schemas_or_err.detail or "" schemas_or_err.detail += "\n\nRecursive allOf reference found" final_model_errors.append((model_prop, schemas_or_err)) @@ -407,7 +424,7 @@ def _process_models(*, schemas: Schemas, config: Config) -> Schemas: def build_schemas( *, - components: dict[str, oai.Reference | oai.Schema], + components: dict[UntrustedString, oai.Reference | oai.Schema], schemas: Schemas, config: Config, ) -> Schemas: @@ -419,12 +436,12 @@ def build_schemas( def build_parameters( *, - components: dict[str, oai.Reference | oai.Parameter], + components: dict[UntrustedString, oai.Reference | oai.Parameter], parameters: Parameters, config: Config, ) -> Parameters: """Get a list of Parameters from an OpenAPI dict""" - to_process: Iterable[tuple[str, oai.Reference | oai.Parameter]] = [] + to_process: Iterable[tuple[UntrustedString, oai.Reference | oai.Parameter]] = [] if components is not None: to_process = components.items() still_making_progress = True @@ -437,17 +454,17 @@ def build_parameters( next_round = [] # Only accumulate errors from the last round, since we might fix some along the way for name, data in to_process: - if isinstance(data, oai.Reference): + if isinstance(data, oai.Reference): # pragma: no cover parameters.errors.append(ParameterError(data=data, detail="Reference parameters are not supported.")) continue - ref_path = parse_reference_path(f"#/components/parameters/{name}") - if isinstance(ref_path, ParseError): + ref_path = parse_reference_path(Ref(f"#/components/parameters/{name.get_untrusted_value()}")) + if isinstance(ref_path, ParseError): # pragma: no cover parameters.errors.append(ParameterError(detail=ref_path.detail, data=data)) continue parameters_or_err = update_parameters_with_data( ref_path=ref_path, data=data, parameters=parameters, config=config ) - if isinstance(parameters_or_err, ParameterError): + if isinstance(parameters_or_err, ParameterError): # pragma: no cover next_round.append((name, data)) errors.append(parameters_or_err) continue diff --git a/openapi_python_client/parser/properties/any.py b/openapi_python_client/parser/properties/any.py index b760a1568..722c304fa 100644 --- a/openapi_python_client/parser/properties/any.py +++ b/openapi_python_client/parser/properties/any.py @@ -2,10 +2,11 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field -from ...utils import PythonIdentifier -from .protocol import PropertyProtocol, Value +from ...schema.untrusted_string import UntrustedString +from ...strings import PythonCode, PythonIdentifier +from .protocol import PropertyProtocol, Value, convert_example @define @@ -15,12 +16,13 @@ class AnyProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + *, + name: UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: UntrustedString | None, + example: Any, ) -> AnyProperty: return cls( name=name, @@ -33,19 +35,19 @@ def build( @classmethod def convert_value(cls, value: Any) -> Value | None: - from .string import StringProperty + from .string import StringProperty # noqa: PLC0415 if value is None: return value if isinstance(value, str): return StringProperty.convert_value(value) - return Value(python_code=str(value), raw_value=value) + return Value(python_code=PythonCode(str(value)), raw_value=value) - name: str + name: UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: UntrustedString | None + example: UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "Any" _json_type_string: ClassVar[str] = "Any" diff --git a/openapi_python_client/parser/properties/boolean.py b/openapi_python_client/parser/properties/boolean.py index 5fd4235d7..5712081b6 100644 --- a/openapi_python_client/parser/properties/boolean.py +++ b/openapi_python_client/parser/properties/boolean.py @@ -2,24 +2,24 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class BooleanProperty(PropertyProtocol): """Property for bool""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "bool" _json_type_string: ClassVar[str] = "bool" @@ -34,12 +34,13 @@ class BooleanProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + *, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> BooleanProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -54,14 +55,14 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if isinstance(value, Value) or value is None: return value if isinstance(value, str): if value.lower() == "true": - return Value(python_code="True", raw_value=value) + return Value(python_code=PythonCode("True"), raw_value=value) elif value.lower() == "false": - return Value(python_code="False", raw_value=value) + return Value(python_code=PythonCode("False"), raw_value=value) if isinstance(value, bool): - return Value(python_code=str(value), raw_value=value) + return Value(python_code=PythonCode(str(value)), raw_value=value) return PropertyError(f"Invalid boolean value: {value}") diff --git a/openapi_python_client/parser/properties/const.py b/openapi_python_client/parser/properties/const.py index 8b9967f19..9480c3e05 100644 --- a/openapi_python_client/parser/properties/const.py +++ b/openapi_python_client/parser/properties/const.py @@ -4,7 +4,8 @@ from attr import define -from ...utils import PythonIdentifier +from ... import schema as oai +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError from .protocol import PropertyProtocol, Value from .string import StringProperty @@ -14,12 +15,12 @@ class ConstProperty(PropertyProtocol): """A property representing a const value""" - name: str + name: oai.UntrustedString required: bool value: Value default: Value | None python_name: PythonIdentifier - description: str | None + description: oai.UntrustedString | None example: None template: ClassVar[str] = "const_property.py.jinja" @@ -29,10 +30,10 @@ def build( *, const: str | int | float | bool, default: Any, - name: str, + name: oai.UntrustedString, python_name: PythonIdentifier, required: bool, - description: str | None, + description: oai.UntrustedString | None, ) -> ConstProperty | PropertyError: """ Create a `ConstProperty` the right way. @@ -62,7 +63,7 @@ def build( prop.default = converted_default return prop - def convert_value(self, value: Any) -> Value | None | PropertyError: + def convert_value(self, value: Any) -> Value | PropertyError | None: value = self._convert_value(value) if value is None: return value @@ -87,19 +88,16 @@ def _convert_value(value: Any) -> Value | None: return value if isinstance(value, str): return StringProperty.convert_value(value) - return Value(python_code=str(value), raw_value=value) + return Value(python_code=PythonCode(str(value)), raw_value=value) def get_type_string( self, no_optional: bool = False, json: bool = False, - *, - multipart: bool = False, - quoted: bool = False, - ) -> str: - lit = f"Literal[{self.value.python_code}]" + ) -> PythonCode: + lit = PythonCode(f"Literal[{self.value.python_code.as_unembedded_code()}]") if not no_optional and not self.required: - return f"Union[{lit}, Unset]" + return PythonCode(f"{lit.as_unembedded_code()} | Unset") return lit def get_imports(self, *, prefix: str) -> set[str]: @@ -113,6 +111,6 @@ def get_imports(self, *, prefix: str) -> set[str]: if self.required: return {"from typing import Literal, cast"} return { - "from typing import Literal, Union, cast", + "from typing import Literal, cast", f"from {prefix}types import UNSET, Unset", } diff --git a/openapi_python_client/parser/properties/date.py b/openapi_python_client/parser/properties/date.py index 7261698ea..14de4026c 100644 --- a/openapi_python_client/parser/properties/date.py +++ b/openapi_python_client/parser/properties/date.py @@ -1,25 +1,26 @@ from __future__ import annotations +import datetime from typing import Any, ClassVar -from attr import define -from dateutil.parser import isoparse +from attr import define, field -from ...utils import PythonIdentifier +from ... import schema as oai +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class DateProperty(PropertyProtocol): """A property of type datetime.date""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "datetime.date" _json_type_string: ClassVar[str] = "str" @@ -28,12 +29,13 @@ class DateProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + *, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> DateProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -49,15 +51,15 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if isinstance(value, Value) or value is None: return value if isinstance(value, str): try: - isoparse(value).date() # make sure it's a valid value + datetime.date.fromisoformat(value) # make sure it's a valid value except ValueError as e: return PropertyError(f"Invalid date: {e}") - return Value(python_code=f"isoparse({value!r}).date()", raw_value=value) + return Value(python_code=PythonCode(f"datetime.date.fromisoformat({value!r})"), raw_value=value) return PropertyError(f"Cannot convert {value} to a date") def get_imports(self, *, prefix: str) -> set[str]: @@ -69,5 +71,5 @@ def get_imports(self, *, prefix: str) -> set[str]: back to the root of the generated client. """ imports = super().get_imports(prefix=prefix) - imports.update({"import datetime", "from typing import cast", "from dateutil.parser import isoparse"}) + imports.update({"import datetime", "from typing import cast"}) return imports diff --git a/openapi_python_client/parser/properties/datetime.py b/openapi_python_client/parser/properties/datetime.py index 5924d173c..4b2de3665 100644 --- a/openapi_python_client/parser/properties/datetime.py +++ b/openapi_python_client/parser/properties/datetime.py @@ -1,13 +1,14 @@ from __future__ import annotations +import datetime from typing import Any, ClassVar -from attr import define -from dateutil.parser import isoparse +from attr import define, field -from ...utils import PythonIdentifier +from ... import schema as oai +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define @@ -16,12 +17,12 @@ class DateTimeProperty(PropertyProtocol): A property of type datetime.datetime """ - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "datetime.datetime" _json_type_string: ClassVar[str] = "str" @@ -30,12 +31,13 @@ class DateTimeProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + *, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> DateTimeProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -51,15 +53,15 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if value is None or isinstance(value, Value): return value if isinstance(value, str): try: - isoparse(value) # make sure it's a valid value + datetime.datetime.fromisoformat(value) # make sure it's a valid value except ValueError as e: return PropertyError(f"Invalid datetime: {e}") - return Value(python_code=f"isoparse({value!r})", raw_value=value) + return Value(python_code=PythonCode(f"datetime.datetime.fromisoformat({value!r})"), raw_value=value) return PropertyError(f"Cannot convert {value} to a datetime") def get_imports(self, *, prefix: str) -> set[str]: @@ -71,5 +73,5 @@ def get_imports(self, *, prefix: str) -> set[str]: back to the root of the generated client. """ imports = super().get_imports(prefix=prefix) - imports.update({"import datetime", "from typing import cast", "from dateutil.parser import isoparse"}) + imports.update({"import datetime", "from typing import cast"}) return imports diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py index fc7f20bd9..573d5f0d8 100644 --- a/openapi_python_client/parser/properties/enum_property.py +++ b/openapi_python_client/parser/properties/enum_property.py @@ -2,33 +2,33 @@ __all__ = ["EnumProperty", "ValueType"] -from typing import Any, ClassVar, Union, cast +from typing import Any, ClassVar, cast from attr import evolve -from attrs import define +from attrs import define, field -from ... import Config, utils +from ... import Config, strings from ... import schema as oai from ...schema import DataType from ..errors import PropertyError from .none import NoneProperty -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example from .schemas import Class, Schemas from .union import UnionProperty -ValueType = Union[str, int] +ValueType = str | int @define class EnumProperty(PropertyProtocol): """A property that should use an enum""" - name: str + name: oai.UntrustedString required: bool default: Value | None - python_name: utils.PythonIdentifier - description: str | None - example: str | None + python_name: strings.PythonIdentifier + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) values: dict[str, ValueType] class_info: Class value_type: type[ValueType] @@ -47,7 +47,7 @@ def build( # noqa: PLR0911 cls, *, data: oai.Schema, - name: str, + name: oai.UntrustedString, required: bool, schemas: Schemas, parent_name: str, @@ -83,7 +83,7 @@ def build( # noqa: PLR0911 name=name, required=required, default="None", - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=None, example=None, ), @@ -98,9 +98,7 @@ def build( # noqa: PLR0911 value_type = next(iter(value_types)) if value_type not in (str, int): return PropertyError(header=f"Unsupported enum type {value_type}", data=data), schemas - value_list = cast( - Union[list[int], list[str]], unchecked_value_list - ) # We checked this with all the value_types stuff + value_list = cast(list[int] | list[str], unchecked_value_list) # We checked this with all the value_types stuff if len(value_list) < len(enum): # Only one of the values was None, that becomes a union data.oneOf = [ @@ -117,11 +115,12 @@ def build( # noqa: PLR0911 config=config, ) - class_name = data.title or name + class_name: str | oai.UntrustedString = data.title or name if parent_name: - class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" + class_name = f"{strings.pascal_case(parent_name)}{strings.pascal_case(class_name)}" class_info = Class.from_string(string=class_name, config=config) - values = EnumProperty.values_from_list(value_list, class_info) + var_names = data.model_extra.get("x-enum-varnames", []) if data.model_extra else [] + values = EnumProperty.values_from_list(value_list, class_info, var_names) if class_info.name in schemas.classes_by_name: existing = schemas.classes_by_name[class_info.name] @@ -140,7 +139,7 @@ def build( # noqa: PLR0911 values=values, value_type=value_type, default=None, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ) @@ -159,16 +158,19 @@ def convert_value(self, value: Any) -> Value | PropertyError | None: if isinstance(value, self.value_type): inverse_values = {v: k for k, v in self.values.items()} try: - return Value(python_code=f"{self.class_info.name}.{inverse_values[value]}", raw_value=value) + return Value( + python_code=strings.PythonCode(f"{self.class_info.name}.{inverse_values[value]}"), + raw_value=value, + ) except KeyError: return PropertyError(detail=f"Value {value} is not valid for enum {self.name}") return PropertyError(detail=f"Cannot convert {value} to enum {self.name} of type {self.value_type}") - def get_base_type_string(self, *, quoted: bool = False) -> str: - return self.class_info.name + def get_base_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.class_info.name) - def get_base_json_type_string(self, *, quoted: bool = False) -> str: - return self.value_type.__name__ + def get_base_json_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.value_type.__name__) def get_imports(self, *, prefix: str) -> set[str]: """ @@ -183,19 +185,28 @@ def get_imports(self, *, prefix: str) -> set[str]: return imports @staticmethod - def values_from_list(values: list[str] | list[int], class_info: Class) -> dict[str, ValueType]: + def values_from_list( + values: list[str] | list[int], class_info: Class, var_names: list[str] + ) -> dict[str, ValueType]: """Convert a list of values into dict of {name: value}, where value can sometimes be None""" output: dict[str, ValueType] = {} + use_var_names = len(var_names) == len(values) for i, value in enumerate(values): - value = cast(Union[str, int], value) + value = cast(str | int, value) if isinstance(value, int): - if value < 0: + if use_var_names: + key = var_names[i] + sanitized_key = strings.snake_case(key).upper() + output[sanitized_key] = value + elif value < 0: output[f"VALUE_NEGATIVE_{-value}"] = value else: output[f"VALUE_{value}"] = value continue - if value and value[0].isalpha(): + if use_var_names: + key = var_names[i] + elif value and value[0].isalpha(): key = value.upper() else: key = f"VALUE_{i}" @@ -204,6 +215,6 @@ def values_from_list(values: list[str] | list[int], class_info: Class) -> dict[s f"Duplicate key {key} in enum {class_info.module_name}.{class_info.name}; " f"consider setting literal_enums in your config" ) - sanitized_key = utils.snake_case(key).upper() - output[sanitized_key] = utils.remove_string_escapes(value) + sanitized_key = strings.snake_case(key).upper() + output[sanitized_key] = strings.in_double_quote_literal(value) return output diff --git a/openapi_python_client/parser/properties/file.py b/openapi_python_client/parser/properties/file.py index 505876b63..629a08b84 100644 --- a/openapi_python_client/parser/properties/file.py +++ b/openapi_python_client/parser/properties/file.py @@ -2,38 +2,40 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field -from ...utils import PythonIdentifier +from ... import schema as oai +from ...strings import PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol +from .protocol import PropertyProtocol, convert_example @define class FileProperty(PropertyProtocol): """A property used for uploading files""" - name: str + name: oai.UntrustedString required: bool default: None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "File" # Return type of File.to_tuple() - _json_type_string: ClassVar[str] = "FileJsonType" + _json_type_string: ClassVar[str] = "FileTypes" template: ClassVar[str] = "file_property.py.jinja" @classmethod def build( cls, - name: str, + *, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> FileProperty | PropertyError: default_or_err = cls.convert_value(default) if isinstance(default_or_err, PropertyError): @@ -49,7 +51,7 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> None | PropertyError: + def convert_value(cls, value: Any) -> PropertyError | None: if value is not None: return PropertyError(detail="File properties cannot have a default value") return value @@ -63,5 +65,5 @@ def get_imports(self, *, prefix: str) -> set[str]: back to the root of the generated client. """ imports = super().get_imports(prefix=prefix) - imports.update({f"from {prefix}types import File, FileJsonType", "from io import BytesIO"}) + imports.update({f"from {prefix}types import File, FileTypes", "from io import BytesIO"}) return imports diff --git a/openapi_python_client/parser/properties/float.py b/openapi_python_client/parser/properties/float.py index a785db6d4..ed9a49683 100644 --- a/openapi_python_client/parser/properties/float.py +++ b/openapi_python_client/parser/properties/float.py @@ -2,24 +2,24 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class FloatProperty(PropertyProtocol): """A property of type float""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "float" _json_type_string: ClassVar[str] = "float" @@ -34,12 +34,13 @@ class FloatProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + *, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> FloatProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -55,17 +56,17 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if isinstance(value, Value) or value is None: return value if isinstance(value, str): try: parsed = float(value) - return Value(python_code=str(parsed), raw_value=value) + return Value(python_code=PythonCode(str(parsed)), raw_value=value) except ValueError: return PropertyError(f"Invalid float value: {value}") if isinstance(value, float): - return Value(python_code=str(value), raw_value=value) + return Value(python_code=PythonCode(str(value)), raw_value=value) if isinstance(value, int) and not isinstance(value, bool): - return Value(python_code=str(float(value)), raw_value=value) + return Value(python_code=PythonCode(str(float(value))), raw_value=value) return PropertyError(f"Cannot convert {value} to a float") diff --git a/openapi_python_client/parser/properties/int.py b/openapi_python_client/parser/properties/int.py index 1cd340fbd..6ee23fcf9 100644 --- a/openapi_python_client/parser/properties/int.py +++ b/openapi_python_client/parser/properties/int.py @@ -2,24 +2,24 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class IntProperty(PropertyProtocol): """A property of type int""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "int" _json_type_string: ClassVar[str] = "int" @@ -34,12 +34,13 @@ class IntProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + *, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> IntProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -55,7 +56,7 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if value is None or isinstance(value, Value): return value converted = value @@ -69,5 +70,5 @@ def convert_value(cls, value: Any) -> Value | None | PropertyError: if converted == as_int: converted = as_int if isinstance(converted, int) and not isinstance(converted, bool): - return Value(python_code=str(converted), raw_value=value) + return Value(python_code=PythonCode(str(converted)), raw_value=value) return PropertyError(f"Invalid int value: {value}") diff --git a/openapi_python_client/parser/properties/list_property.py b/openapi_python_client/parser/properties/list_property.py index bf0756fe3..e331cddb4 100644 --- a/openapi_python_client/parser/properties/list_property.py +++ b/openapi_python_client/parser/properties/list_property.py @@ -2,12 +2,12 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field -from ... import Config, utils +from ... import Config, strings from ... import schema as oai from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example from .schemas import ReferencePath, Schemas @@ -15,12 +15,12 @@ class ListProperty(PropertyProtocol): """A property representing a list (array) of other properties""" - name: str + name: oai.UntrustedString required: bool default: Value | None - python_name: utils.PythonIdentifier - description: str | None - example: str | None + python_name: strings.PythonIdentifier + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) inner_property: PropertyProtocol template: ClassVar[str] = "list_property.py.jinja" @@ -29,13 +29,13 @@ def build( cls, *, data: oai.Schema, - name: str, + name: oai.UntrustedString, required: bool, schemas: Schemas, parent_name: str, config: Config, process_properties: bool, - roots: set[ReferencePath | utils.ClassName], + roots: set[ReferencePath | strings.ClassName], ) -> tuple[ListProperty | PropertyError, Schemas]: """ Build a ListProperty the right way, use this instead of the normal constructor. @@ -56,7 +56,7 @@ def build( `(result, schemas)` where `schemas` is an updated version of the input named the same including any inner classes that were defined and `result` is either the `ListProperty` or a `PropertyError`. """ - from . import property_from_data + from . import property_from_data # noqa: PLC0415 if data.items is None and not data.prefixItems: return ( @@ -77,7 +77,7 @@ def build( inner_schema = oai.Schema(anyOf=items) inner_prop, schemas = property_from_data( - name=f"{name}_item", + name=oai.UntrustedString(f"{name.get_untrusted_value()}_item"), required=True, data=inner_schema, schemas=schemas, @@ -95,25 +95,25 @@ def build( required=required, default=None, inner_property=inner_prop, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ), schemas, ) - def convert_value(self, value: Any) -> Value | None | PropertyError: + def convert_value(self, value: Any) -> Value | PropertyError | None: return None # pragma: no cover - def get_base_type_string(self, *, quoted: bool = False) -> str: - return f"list[{self.inner_property.get_type_string(quoted=not self.inner_property.is_base_type)}]" + def get_base_type_string(self) -> strings.PythonCode: + return strings.PythonCode(f"list[{self.inner_property.get_type_string().as_unembedded_code()}]") - def get_base_json_type_string(self, *, quoted: bool = False) -> str: - return f"list[{self.inner_property.get_type_string(json=True, quoted=not self.inner_property.is_base_type)}]" + def get_base_json_type_string(self) -> strings.PythonCode: + return strings.PythonCode(f"list[{self.inner_property.get_type_string(json=True).as_unembedded_code()}]") - def get_instance_type_string(self) -> str: - """Get a string representation of runtime type that should be used for `isinstance` checks""" - return "list" + def get_instance_type_string(self) -> strings.PythonCode: + """Get a code representation of runtime type that should be used for `isinstance` checks""" + return strings.PythonCode("list") def get_imports(self, *, prefix: str) -> set[str]: """ @@ -137,12 +137,9 @@ def get_type_string( self, no_optional: bool = False, json: bool = False, - *, - multipart: bool = False, - quoted: bool = False, - ) -> str: + ) -> strings.PythonCode: """ - Get a string representation of type that should be used when declaring this property + Get a Python code representation of type that should be used when declaring this property Args: no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) @@ -150,11 +147,9 @@ def get_type_string( """ if json: type_string = self.get_base_json_type_string() - elif multipart: - type_string = "tuple[None, bytes, str]" else: type_string = self.get_base_type_string() if no_optional or self.required: return type_string - return f"Union[Unset, {type_string}]" + return strings.PythonCode(f"{type_string.as_unembedded_code()} | Unset") diff --git a/openapi_python_client/parser/properties/literal_enum_property.py b/openapi_python_client/parser/properties/literal_enum_property.py index 669b62f58..0388fe04e 100644 --- a/openapi_python_client/parser/properties/literal_enum_property.py +++ b/openapi_python_client/parser/properties/literal_enum_property.py @@ -2,33 +2,33 @@ __all__ = ["LiteralEnumProperty"] -from typing import Any, ClassVar, Union, cast +from typing import Any, ClassVar, cast from attr import evolve -from attrs import define +from attrs import define, field -from ... import Config, utils +from ... import Config, strings from ... import schema as oai from ...schema import DataType from ..errors import PropertyError from .none import NoneProperty -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example from .schemas import Class, Schemas from .union import UnionProperty -ValueType = Union[str, int] +ValueType = str | int @define class LiteralEnumProperty(PropertyProtocol): """A property that should use a literal enum""" - name: str + name: oai.UntrustedString required: bool default: Value | None - python_name: utils.PythonIdentifier - description: str | None - example: str | None + python_name: strings.PythonIdentifier + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) values: set[ValueType] class_info: Class value_type: type[ValueType] @@ -47,7 +47,7 @@ def build( # noqa: PLR0911 cls, *, data: oai.Schema, - name: str, + name: oai.UntrustedString, required: bool, schemas: Schemas, parent_name: str, @@ -82,7 +82,7 @@ def build( # noqa: PLR0911 name=name, required=required, default="None", - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=None, example=None, ), @@ -97,9 +97,7 @@ def build( # noqa: PLR0911 value_type = next(iter(value_types)) if value_type not in (str, int): return PropertyError(header=f"Unsupported enum type {value_type}", data=data), schemas - value_list = cast( - Union[list[int], list[str]], unchecked_value_list - ) # We checked this with all the value_types stuff + value_list = cast(list[int] | list[str], unchecked_value_list) # We checked this with all the value_types stuff if len(value_list) < len(enum): # Only one of the values was None, that becomes a union data.oneOf = [ @@ -116,9 +114,9 @@ def build( # noqa: PLR0911 config=config, ) - class_name = data.title or name + class_name: str | oai.UntrustedString = data.title or name if parent_name: - class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" + class_name = f"{strings.pascal_case(parent_name)}{strings.pascal_case(class_name)}" class_info = Class.from_string(string=class_name, config=config) values: set[str | int] = set(value_list) @@ -139,7 +137,7 @@ def build( # noqa: PLR0911 values=values, value_type=value_type, default=None, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, ) @@ -157,19 +155,19 @@ def convert_value(self, value: Any) -> Value | PropertyError | None: return value if isinstance(value, self.value_type): if value in self.values: - return Value(python_code=repr(value), raw_value=value) + return Value(python_code=strings.PythonCode(repr(value)), raw_value=value) else: return PropertyError(detail=f"Value {value} is not valid for enum {self.name}") return PropertyError(detail=f"Cannot convert {value} to enum {self.name} of type {self.value_type}") - def get_base_type_string(self, *, quoted: bool = False) -> str: - return self.class_info.name + def get_base_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.class_info.name) - def get_base_json_type_string(self, *, quoted: bool = False) -> str: - return self.value_type.__name__ + def get_base_json_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.value_type.__name__) - def get_instance_type_string(self) -> str: - return self.value_type.__name__ + def get_instance_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.value_type.__name__) def get_imports(self, *, prefix: str) -> set[str]: """ @@ -188,4 +186,4 @@ def get_imports(self, *, prefix: str) -> set[str]: return imports def get_class_name_snake_case(self) -> str: - return utils.snake_case(self.class_info.name) + return strings.snake_case(self.class_info.name) diff --git a/openapi_python_client/parser/properties/merge_properties.py b/openapi_python_client/parser/properties/merge_properties.py index db6424a7c..2ccf7f9e4 100644 --- a/openapi_python_client/parser/properties/merge_properties.py +++ b/openapi_python_client/parser/properties/merge_properties.py @@ -67,11 +67,12 @@ def merge_properties(prop1: Property, prop2: Property) -> Property | PropertyErr return merged return PropertyError( - detail=f"{prop1.get_type_string(no_optional=True)} can't be merged with {prop2.get_type_string(no_optional=True)}" + detail=f"{prop1.get_type_string(no_optional=True).as_unembedded_code()} can't be merged with " + f"{prop2.get_type_string(no_optional=True).as_unembedded_code()}" ) -def _merge_same_type(prop1: Property, prop2: Property) -> Property | None | PropertyError: +def _merge_same_type(prop1: Property, prop2: Property) -> Property | PropertyError | None: if type(prop1) is not type(prop2): return None @@ -90,7 +91,7 @@ def _merge_same_type(prop1: Property, prop2: Property) -> Property | None | Prop return _merge_common_attributes(prop1, prop2) -def _merge_string_with_format(prop1: Property, prop2: Property) -> Property | None | PropertyError: +def _merge_string_with_format(prop1: Property, prop2: Property) -> Property | PropertyError | None: """Merge a string that has no format with a string that has a format""" # Here we need to use the DateProperty/DateTimeProperty/FileProperty as the base so that we preserve # its class, but keep the correct override order for merging the attributes. @@ -103,11 +104,11 @@ def _merge_string_with_format(prop1: Property, prop2: Property) -> Property | No return None -def _merge_numeric(prop1: Property, prop2: Property) -> IntProperty | None | PropertyError: +def _merge_numeric(prop1: Property, prop2: Property) -> IntProperty | PropertyError | None: """Merge IntProperty with FloatProperty""" - if isinstance(prop1, IntProperty) and isinstance(prop2, (IntProperty, FloatProperty)): + if isinstance(prop1, IntProperty) and isinstance(prop2, IntProperty | FloatProperty): return _merge_common_attributes(prop1, prop2) - elif isinstance(prop2, IntProperty) and isinstance(prop1, (IntProperty, FloatProperty)): + elif isinstance(prop2, IntProperty) and isinstance(prop1, IntProperty | FloatProperty): # Use the IntProperty as a base since it's more restrictive, but keep the correct override order return _merge_common_attributes(prop2, prop1, prop2) else: @@ -136,7 +137,8 @@ def _merge_with_enum(prop1: PropertyProtocol, prop2: PropertyProtocol) -> EnumPr ): return _merge_common_attributes(enum_prop, prop1, prop2) return PropertyError( - detail=f"can't combine enum of type {enum_prop.value_type} with {non_enum_prop.get_type_string(no_optional=True)}" + detail=f"can't combine enum of type {enum_prop.value_type} with " + f"{non_enum_prop.get_type_string(no_optional=True).as_unembedded_code()}" ) @@ -162,7 +164,8 @@ def _merge_with_literal_enum(prop1: PropertyProtocol, prop2: PropertyProtocol) - ): return _merge_common_attributes(enum_prop, prop1, prop2) return PropertyError( - detail=f"can't combine literal enum of type {enum_prop.value_type} with {non_enum_prop.get_type_string(no_optional=True)}" + detail=f"can't combine literal enum of type {enum_prop.value_type} with " + f"{non_enum_prop.get_type_string(no_optional=True).as_unembedded_code()}" ) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 762624501..3f6548e77 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -3,14 +3,14 @@ from itertools import chain from typing import Any, ClassVar, NamedTuple -from attrs import define, evolve +from attrs import define, evolve, field -from ... import Config, utils +from ... import Config, strings from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonIdentifier from ..errors import ParseError, PropertyError from .any import AnyProperty -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example from .schemas import Class, ReferencePath, Schemas, parse_reference_path @@ -18,15 +18,15 @@ class ModelProperty(PropertyProtocol): """A property which refers to another Schema""" - name: str + name: oai.UntrustedString required: bool default: Value | None - python_name: utils.PythonIdentifier - example: str | None + python_name: strings.PythonIdentifier + example: oai.UntrustedString | None = field(converter=convert_example) class_info: Class data: oai.Schema - description: str - roots: set[ReferencePath | utils.ClassName] + description: oai.UntrustedString | None + roots: set[ReferencePath | strings.ClassName] required_properties: list[Property] | None optional_properties: list[Property] | None relative_imports: set[str] | None @@ -43,13 +43,13 @@ def build( cls, *, data: oai.Schema, - name: str, + name: oai.UntrustedString, schemas: Schemas, required: bool, parent_name: str | None, config: Config, process_properties: bool, - roots: set[ReferencePath | utils.ClassName], + roots: set[ReferencePath | strings.ClassName], ) -> tuple[ModelProperty | PropertyError, Schemas]: """ A single ModelProperty from its OAI data @@ -65,12 +65,13 @@ def build( roots: Set of strings that identify schema objects on which the new ModelProperty will depend process_properties: Determines whether the new ModelProperty will be initialized with property data """ + class_string: str | oai.UntrustedString if not config.use_path_prefixes_for_title_model_names and data.title: class_string = data.title else: title = data.title or name if parent_name: - class_string = f"{utils.pascal_case(parent_name)}{utils.pascal_case(title)}" + class_string = f"{strings.pascal_case(parent_name)}{strings.pascal_case(title)}" else: class_string = title class_info = Class.from_string(string=class_string, config=config) @@ -92,7 +93,7 @@ def build( relative_imports = property_data.relative_imports lazy_imports = property_data.lazy_imports for root in roots: - if isinstance(root, utils.ClassName): + if isinstance(root, strings.ClassName): continue schemas.add_dependencies(root, {class_info.name}) @@ -105,11 +106,11 @@ def build( relative_imports=relative_imports, lazy_imports=lazy_imports, additional_properties=additional_properties, - description=data.description or "", + description=data.description, default=None, required=required, name=name, - python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), example=data.example, ) if class_info.name in schemas.classes_by_name: @@ -126,7 +127,7 @@ def build( return prop, schemas @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if value is not None: return PropertyError(detail="ModelProperty cannot have a default value") # pragma: no cover return None @@ -140,8 +141,8 @@ def self_import(self) -> str: """Constructs a self import statement from this ModelProperty's attributes""" return f"models.{self.class_info.module_name} import {self.class_info.name}" - def get_base_type_string(self, *, quoted: bool = False) -> str: - return f'"{self.class_info.name}"' if quoted else self.class_info.name + def get_base_type_string(self) -> strings.PythonCode: + return strings.PythonCode(self.class_info.name) def get_imports(self, *, prefix: str) -> set[str]: """ @@ -188,12 +189,9 @@ def get_type_string( self, no_optional: bool = False, json: bool = False, - *, - multipart: bool = False, - quoted: bool = False, - ) -> str: + ) -> strings.PythonCode: """ - Get a string representation of type that should be used when declaring this property + Get a Python code representation of type that should be used when declaring this property Args: no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) @@ -201,18 +199,12 @@ def get_type_string( """ if json: type_string = self.get_base_json_type_string() - elif multipart: - type_string = "tuple[None, bytes, str]" else: type_string = self.get_base_type_string() - if quoted: - if type_string == self.class_info.name: - type_string = f"'{type_string}'" - if no_optional or self.required: return type_string - return f"Union[Unset, {type_string}]" + return strings.PythonCode(f"{type_string.as_unembedded_code()} | Unset") from .property import Property # noqa: E402 @@ -241,14 +233,14 @@ def _process_properties( # noqa: PLR0912, PLR0911 *, data: oai.Schema, schemas: Schemas, - class_name: utils.ClassName, + class_name: strings.ClassName, config: Config, - roots: set[ReferencePath | utils.ClassName], + roots: set[ReferencePath | strings.ClassName], ) -> _PropertyData | PropertyError: - from . import property_from_data - from .merge_properties import merge_properties + from . import property_from_data # noqa: PLC0415 + from .merge_properties import merge_properties # noqa: PLC0415 - properties: dict[str, Property] = {} + properties: dict[oai.UntrustedString, Property] = {} relative_imports: set[str] = set() lazy_imports: set[str] = set() required_set = set(data.required or []) @@ -274,7 +266,7 @@ def _add_if_no_conflict(new_prop: Property) -> PropertyError | None: properties[merged_prop.name] = merged_prop return None - unprocessed_props: list[tuple[str, oai.Reference | oai.Schema]] = ( + unprocessed_props: list[tuple[oai.UntrustedString, oai.Reference | oai.Schema]] = ( list(data.properties.items()) if data.properties else [] ) for sub_prop in data.allOf: @@ -284,7 +276,7 @@ def _add_if_no_conflict(new_prop: Property) -> PropertyError | None: return PropertyError(detail=ref_path.detail, data=sub_prop) sub_model = schemas.classes_by_reference.get(ref_path) if sub_model is None: - return PropertyError(f"Reference {sub_prop.ref} not found") + return PropertyError(f"Reference {sub_prop.ref.get_untrusted_value()} not found") if not isinstance(sub_model, ModelProperty): return PropertyError("Cannot take allOf a non-object") # Properties of allOf references first should be processed first @@ -301,6 +293,11 @@ def _add_if_no_conflict(new_prop: Property) -> PropertyError | None: unprocessed_props.extend(sub_prop.properties.items() if sub_prop.properties else []) required_set.update(sub_prop.required or []) + # Update properties that are marked as required in the schema + for prop_name in required_set: + if prop_name in properties and not properties[prop_name].required: + properties[prop_name] = evolve(properties[prop_name], required=True) + for key, value in unprocessed_props: prop_required = key in required_set prop_or_error: Property | (PropertyError | None) @@ -339,10 +336,10 @@ def _add_if_no_conflict(new_prop: Property) -> PropertyError | None: ANY_ADDITIONAL_PROPERTY = AnyProperty.build( - name="additional", + name=oai.UntrustedString("additional"), required=True, default=None, - description="", + description=None, python_name=PythonIdentifier(value="additional", prefix=""), example=None, ) @@ -350,13 +347,13 @@ def _add_if_no_conflict(new_prop: Property) -> PropertyError | None: def _get_additional_properties( *, - schema_additional: None | (bool | (oai.Reference | oai.Schema)), + schema_additional: bool | oai.Reference | oai.Schema | None, schemas: Schemas, - class_name: utils.ClassName, + class_name: strings.ClassName, config: Config, - roots: set[ReferencePath | utils.ClassName], -) -> tuple[Property | None | PropertyError, Schemas]: - from . import property_from_data + roots: set[ReferencePath | strings.ClassName], +) -> tuple[Property | PropertyError | None, Schemas]: + from . import property_from_data # noqa: PLC0415 if schema_additional is None: return ANY_ADDITIONAL_PROPERTY, schemas @@ -371,7 +368,7 @@ def _get_additional_properties( return ANY_ADDITIONAL_PROPERTY, schemas additional_properties, schemas = property_from_data( - name="AdditionalProperty", + name=oai.UntrustedString("AdditionalProperty"), required=True, # in the sense that if present in the dict will not be None data=schema_additional, schemas=schemas, @@ -388,7 +385,7 @@ def _process_property_data( schemas: Schemas, class_info: Class, config: Config, - roots: set[ReferencePath | utils.ClassName], + roots: set[ReferencePath | strings.ClassName], ) -> tuple[tuple[_PropertyData, Property | None] | PropertyError, Schemas]: property_data = _process_properties( data=data, schemas=schemas, class_name=class_info.name, config=config, roots=roots diff --git a/openapi_python_client/parser/properties/none.py b/openapi_python_client/parser/properties/none.py index 9c473693d..aae5e631b 100644 --- a/openapi_python_client/parser/properties/none.py +++ b/openapi_python_client/parser/properties/none.py @@ -2,24 +2,24 @@ from typing import Any, ClassVar -from attr import define +from attr import define, field from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class NoneProperty(PropertyProtocol): """A property that can only be None""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _allowed_locations: ClassVar[set[oai.ParameterLocation]] = { oai.ParameterLocation.QUERY, @@ -32,12 +32,13 @@ class NoneProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + *, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> NoneProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -52,10 +53,10 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if value is None or isinstance(value, Value): return value if isinstance(value, str): if value == "None": - return Value(python_code=value, raw_value=value) + return Value(python_code=PythonCode(value), raw_value=value) return PropertyError(f"Value {value} is not valid, only None is allowed") diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index 6e73a01ae..5927c6e91 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -1,8 +1,7 @@ __all__ = ["Property"] -from typing import Union -from typing_extensions import TypeAlias +from typing import TypeAlias from .any import AnyProperty from .boolean import BooleanProperty @@ -21,21 +20,21 @@ from .union import UnionProperty from .uuid import UuidProperty -Property: TypeAlias = Union[ - AnyProperty, - BooleanProperty, - ConstProperty, - DateProperty, - DateTimeProperty, - EnumProperty, - LiteralEnumProperty, - FileProperty, - FloatProperty, - IntProperty, - ListProperty, - ModelProperty, - NoneProperty, - StringProperty, - UnionProperty, - UuidProperty, -] +Property: TypeAlias = ( + AnyProperty + | BooleanProperty + | ConstProperty + | DateProperty + | DateTimeProperty + | EnumProperty + | LiteralEnumProperty + | FileProperty + | FloatProperty + | IntProperty + | ListProperty + | ModelProperty + | NoneProperty + | StringProperty + | UnionProperty + | UuidProperty +) diff --git a/openapi_python_client/parser/properties/protocol.py b/openapi_python_client/parser/properties/protocol.py index 9a5b51828..d4fabb014 100644 --- a/openapi_python_client/parser/properties/protocol.py +++ b/openapi_python_client/parser/properties/protocol.py @@ -1,20 +1,29 @@ from __future__ import annotations -__all__ = ["PropertyProtocol", "Value"] +__all__ = ["PropertyProtocol", "Value", "convert_example"] from abc import abstractmethod from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar +from typing import Any, ClassVar, Protocol, TypeVar from ... import Config from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier, safe_for_docstring from ..errors import ParseError, PropertyError -if TYPE_CHECKING: # pragma: no cover - from .model_property import ModelProperty -else: - ModelProperty = "ModelProperty" + +def convert_example(example: Any) -> oai.UntrustedString | None: + """ + Convert a raw OpenAPI `example` (which can be anything) into an `UntrustedString` (or None). + + Examples only ever appear in docstrings, so non-string examples are stringified. Wrapping ensures a + template which uses an example without escaping renders a safe-but-broken value instead of an exploit. + """ + if example is None or isinstance(example, oai.UntrustedString): + return example + if isinstance(example, str): + return oai.UntrustedString(example) + return oai.UntrustedString(str(example)) @dataclass @@ -24,7 +33,7 @@ class Value: (with string escaping, for example). We still keep the `raw_value` around for merging `allOf`. """ - python_code: str + python_code: PythonCode raw_value: Any @@ -45,7 +54,7 @@ class PropertyProtocol(Protocol): ValidationError: Raised when the default value fails to be converted to the expected type """ - name: str + name: oai.UntrustedString required: bool _type_string: ClassVar[str] = "" _json_type_string: ClassVar[str] = "" # Type of the property after JSON serialization @@ -56,26 +65,26 @@ class PropertyProtocol(Protocol): } default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None template: ClassVar[str] = "any_property.py.jinja" json_is_dict: ClassVar[bool] = False @abstractmethod - def convert_value(self, value: Any) -> Value | None | PropertyError: + def convert_value(self, value: Any) -> Value | PropertyError | None: """Convert a string value to a Value object""" raise NotImplementedError() # pragma: no cover def validate_location(self, location: oai.ParameterLocation) -> ParseError | None: """Returns an error if this type of property is not allowed in the given location""" if location not in self._allowed_locations: - return ParseError(detail=f"{self.get_type_string()} is not allowed in {location}") + return ParseError(detail=f"{self.get_type_string().as_unembedded_code()} is not allowed in {location}") if location == oai.ParameterLocation.PATH and not self.required: return ParseError(detail="Path parameter must be required") return None - def set_python_name(self, new_name: str, config: Config, skip_snake_case: bool = False) -> None: + def set_python_name(self, new_name: oai.UntrustedString, config: Config, skip_snake_case: bool = False) -> None: """Mutates this Property to set a new python_name. Required to mutate due to how Properties are stored and the difficulty of updating them in-dict. @@ -88,45 +97,38 @@ def set_python_name(self, new_name: str, config: Config, skip_snake_case: bool = PythonIdentifier(value=new_name, prefix=config.field_prefix, skip_snake_case=skip_snake_case), ) - def get_base_type_string(self, *, quoted: bool = False) -> str: - """Get the string describing the Python type of this property. Base types no require quoting.""" - return f'"{self._type_string}"' if not self.is_base_type and quoted else self._type_string + def get_base_type_string(self) -> PythonCode: + """Get the code describing the Python type of this property. Base types no require quoting.""" + return PythonCode(self._type_string) - def get_base_json_type_string(self, *, quoted: bool = False) -> str: - """Get the string describing the JSON type of this property. Base types no require quoting.""" - return f'"{self._json_type_string}"' if not self.is_base_type and quoted else self._json_type_string + def get_base_json_type_string(self) -> PythonCode: + """Get the code describing the JSON type of this property. Base types no require quoting.""" + return PythonCode(self._json_type_string) def get_type_string( self, no_optional: bool = False, json: bool = False, - *, - multipart: bool = False, - quoted: bool = False, - ) -> str: + ) -> PythonCode: """ - Get a string representation of type that should be used when declaring this property + Get a Python code representation of type that should be used when declaring this property Args: no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) json: True if the type refers to the property after JSON serialization - multipart: True if the type should be used in a multipart request - quoted: True if the type should be wrapped in quotes (if not a base type) """ if json: - type_string = self.get_base_json_type_string(quoted=quoted) - elif multipart: - type_string = "tuple[None, bytes, str]" + type_string = self.get_base_json_type_string() else: - type_string = self.get_base_type_string(quoted=quoted) + type_string = self.get_base_type_string() if no_optional or self.required: return type_string - return f"Union[Unset, {type_string}]" + return PythonCode(f"{type_string.as_unembedded_code()} | Unset") - def get_instance_type_string(self) -> str: - """Get a string representation of runtime type that should be used for `isinstance` checks""" - return self.get_type_string(no_optional=True, quoted=False) + def get_instance_type_string(self) -> PythonCode: + """Get a Python code representation of runtime type that should be used for `isinstance` checks""" + return self.get_type_string(no_optional=True) # noinspection PyUnusedLocal def get_imports(self, *, prefix: str) -> set[str]: @@ -139,7 +141,6 @@ def get_imports(self, *, prefix: str) -> set[str]: """ imports = set() if not self.required: - imports.add("from typing import Union") imports.add(f"from {prefix}types import UNSET, Unset") return imports @@ -152,36 +153,26 @@ def get_lazy_imports(self, *, prefix: str) -> set[str]: """ return set() - def to_string(self) -> str: + def to_string(self) -> PythonCode: """How this should be declared in a dataclass""" default: str | None if self.default is not None: - default = self.default.python_code + default = self.default.python_code.as_unembedded_code() elif not self.required: default = "UNSET" else: default = None + declaration = f"{self.python_name}: {self.get_type_string().as_unembedded_code()}" if default is not None: - return f"{self.python_name}: {self.get_type_string(quoted=True)} = {default}" - return f"{self.python_name}: {self.get_type_string(quoted=True)}" + declaration += f" = {default}" + return PythonCode(declaration) def to_docstring(self) -> str: """Returns property docstring""" - doc = f"{self.python_name} ({self.get_type_string()}): {self.description or ''}" + doc = f"{self.python_name} ({safe_for_docstring(self.get_type_string())}): {safe_for_docstring(self.description or '')}" if self.default: - doc += f" Default: {self.default.python_code}." + doc += f" Default: {safe_for_docstring(self.default.python_code)}." if self.example: - doc += f" Example: {self.example}." + doc += f" Example: {safe_for_docstring(self.example)}." return doc - - @property - def is_base_type(self) -> bool: - """Base types, represented by any other of `Property` than `ModelProperty` should not be quoted.""" - from . import ListProperty, ModelProperty, UnionProperty - - return self.__class__.__name__ not in { - ModelProperty.__name__, - ListProperty.__name__, - UnionProperty.__name__, - } diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index 177a86924..49e278437 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -10,7 +10,7 @@ "update_schemas_with_data", ] -from typing import TYPE_CHECKING, NewType, Union, cast +from typing import TYPE_CHECKING, NewType from urllib.parse import urlparse from attrs import define, evolve, field @@ -18,7 +18,9 @@ from ... import Config from ... import schema as oai from ...schema.openapi_schema_pydantic import Parameter -from ...utils import ClassName, PythonIdentifier +from ...schema.ref import Ref +from ...schema.untrusted_string import UntrustedString +from ...strings import ClassName, PythonIdentifier from ..errors import ParameterError, ParseError, PropertyError if TYPE_CHECKING: # pragma: no cover @@ -29,10 +31,10 @@ Property = "Property" -ReferencePath = NewType("ReferencePath", str) +ReferencePath = NewType("ReferencePath", UntrustedString) -def parse_reference_path(ref_path_raw: str) -> Union[ReferencePath, ParseError]: +def parse_reference_path(ref_path_raw: Ref) -> ReferencePath | ParseError: """ Takes a raw string provided in a `$ref` and turns it into a validated `_ReferencePath` or a `ParseError` if validation fails. @@ -40,10 +42,17 @@ def parse_reference_path(ref_path_raw: str) -> Union[ReferencePath, ParseError]: See Also: - https://swagger.io/docs/specification/using-ref/ """ - parsed = urlparse(ref_path_raw) + parsed = urlparse(ref_path_raw.get_untrusted_value()) if parsed.scheme or parsed.path: - return ParseError(detail=f"Remote references such as {ref_path_raw} are not supported yet.") - return cast(ReferencePath, parsed.fragment) + return ParseError(detail=f"Remote references such as {parsed} are not supported yet.") + return ReferencePath(UntrustedString(parsed.fragment)) + + +def get_reference_simple_name(ref_path: Ref | UntrustedString) -> UntrustedString: + """ + Takes a path like `/components/schemas/NameOfThing` and returns a string like `NameOfThing`. + """ + return UntrustedString(ref_path.get_untrusted_value().rsplit("/", maxsplit=1)[-1]) @define @@ -54,10 +63,10 @@ class Class: module_name: PythonIdentifier @staticmethod - def from_string(*, string: str, config: Config) -> "Class": + def from_string(*, string: str | UntrustedString, config: Config) -> "Class": """Get a Class from an arbitrary string""" - class_name = string.split("/")[-1] # Get rid of ref path stuff - class_name = ClassName(class_name, config.field_prefix) + untrusted_class_name = get_reference_simple_name(Ref(string)) # Get rid of ref path stuff + class_name = ClassName(untrusted_class_name, config.field_prefix) override = config.class_overrides.get(class_name) if override is not None and override.class_name is not None: @@ -77,12 +86,12 @@ class Schemas: """Structure for containing all defined, shareable, and reusable schemas (attr classes and Enums)""" classes_by_reference: dict[ReferencePath, Property] = field(factory=dict) - dependencies: dict[ReferencePath, set[Union[ReferencePath, ClassName]]] = field(factory=dict) + dependencies: dict[ReferencePath, set[ReferencePath | ClassName]] = field(factory=dict) classes_by_name: dict[ClassName, Property] = field(factory=dict) models_to_process: list[ModelProperty] = field(factory=list) errors: list[ParseError] = field(factory=list) - def add_dependencies(self, ref_path: ReferencePath, roots: set[Union[ReferencePath, ClassName]]) -> None: + def add_dependencies(self, ref_path: ReferencePath, roots: set[ReferencePath | ClassName]) -> None: """Record new dependencies on the given ReferencePath Args: @@ -95,7 +104,7 @@ def add_dependencies(self, ref_path: ReferencePath, roots: set[Union[ReferencePa def update_schemas_with_data( *, ref_path: ReferencePath, data: oai.Schema, schemas: Schemas, config: Config -) -> Union[Schemas, PropertyError]: +) -> Schemas | PropertyError: """ Update a `Schemas` using some new reference. @@ -111,9 +120,9 @@ def update_schemas_with_data( See Also: - https://swagger.io/docs/specification/using-ref/ """ - from . import property_from_data + from . import property_from_data # noqa: PLC0415 - prop: Union[PropertyError, Property] + prop: PropertyError | Property prop, schemas = property_from_data( data=data, name=ref_path, @@ -127,12 +136,15 @@ def update_schemas_with_data( ) if isinstance(prop, PropertyError): - prop.detail = f"{prop.header}: {prop.detail}" - prop.header = f"Unable to parse schema {ref_path}" - if isinstance(prop.data, oai.Reference) and prop.data.ref.endswith(ref_path): # pragma: nocover - prop.detail += ( + detail = f"{prop.header}: {prop.detail}" if prop.detail is not None else prop.header + prop.header = f"Unable to parse schema {ref_path.get_untrusted_value()}" + if isinstance(prop.data, oai.Reference) and prop.data.ref.endswith( + ref_path.get_untrusted_value() + ): # pragma: nocover + detail += ( "\n\nRecursive and circular references are not supported directly in an array schema's 'items' section" ) + prop.detail = detail return prop schemas = evolve(schemas, classes_by_reference={ref_path: prop, **schemas.classes_by_reference}) @@ -150,11 +162,11 @@ class Parameters: def parameter_from_data( *, - name: str, - data: Union[oai.Reference, oai.Parameter], + name: UntrustedString, + data: oai.Reference | oai.Parameter, parameters: Parameters, config: Config, -) -> tuple[Union[Parameter, ParameterError], Parameters]: +) -> tuple[Parameter | ParameterError, Parameters]: """Generates parameters from an OpenAPI Parameter spec.""" if isinstance(data, oai.Reference): @@ -164,10 +176,10 @@ def parameter_from_data( return ParameterError("Parameter has no schema"), parameters new_param = Parameter( - name=name, + name=name.get_untrusted_value(), required=data.required, explode=data.explode, - style=data.style, + style=data.style.get_untrusted_value() if data.style is not None else None, param_schema=data.param_schema, param_in=data.param_in, ) @@ -179,7 +191,7 @@ def parameter_from_data( def update_parameters_with_data( *, ref_path: ReferencePath, data: oai.Parameter, parameters: Parameters, config: Config -) -> Union[Parameters, ParameterError]: +) -> Parameters | ParameterError: """ Update a `Parameters` using some new reference. @@ -196,10 +208,12 @@ def update_parameters_with_data( """ param, parameters = parameter_from_data(data=data, name=data.name, parameters=parameters, config=config) - if isinstance(param, ParameterError): + if isinstance(param, ParameterError): # pragma: no cover param.detail = f"{param.header}: {param.detail}" - param.header = f"Unable to parse parameter {ref_path}" - if isinstance(param.data, oai.Reference) and param.data.ref.endswith(ref_path): # pragma: nocover + param.header = f"Unable to parse parameter {ref_path.get_untrusted_value()}" + if isinstance(param.data, oai.Reference) and param.data.ref.endswith( + ref_path.get_untrusted_value() + ): # pragma: nocover param.detail += ( "\n\nRecursive and circular references are not supported. " "See https://github.com/openapi-generators/openapi-python-client/issues/466" @@ -212,9 +226,9 @@ def update_parameters_with_data( def parameter_from_reference( *, - param: Union[oai.Reference, Parameter], + param: oai.Reference | Parameter, parameters: Parameters, -) -> Union[Parameter, ParameterError]: +) -> Parameter | ParameterError: """ Returns a Parameter from a Reference or the Parameter itself if one was provided. @@ -238,5 +252,5 @@ def parameter_from_reference( _resolved_parameter_class = parameters.classes_by_reference.get(ref_path, None) if _resolved_parameter_class is None: - return ParameterError(detail=f"Reference `{ref_path}` not found.") + return ParameterError(detail=f"Reference `{ref_path.get_untrusted_value()}` not found.") return _resolved_parameter_class diff --git a/openapi_python_client/parser/properties/string.py b/openapi_python_client/parser/properties/string.py index e40c1eee6..a049f127a 100644 --- a/openapi_python_client/parser/properties/string.py +++ b/openapi_python_client/parser/properties/string.py @@ -2,25 +2,24 @@ from typing import Any, ClassVar, overload -from attr import define +from attr import define, field from ... import schema as oai -from ... import utils -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier, remove_string_escapes from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class StringProperty(PropertyProtocol): """A property of type str""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "str" _json_type_string: ClassVar[str] = "str" _allowed_locations: ClassVar[set[oai.ParameterLocation]] = { @@ -33,12 +32,13 @@ class StringProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + *, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> StringProperty | PropertyError: checked_default = cls.convert_value(default) return cls( @@ -65,4 +65,4 @@ def convert_value(cls, value: Any) -> Value | None: return value if not isinstance(value, str): value = str(value) - return Value(python_code=repr(utils.remove_string_escapes(value)), raw_value=value) + return Value(python_code=PythonCode(repr(remove_string_escapes(value))), raw_value=value) diff --git a/openapi_python_client/parser/properties/union.py b/openapi_python_client/parser/properties/union.py index 8b7b02a48..788e803a6 100644 --- a/openapi_python_client/parser/properties/union.py +++ b/openapi_python_client/parser/properties/union.py @@ -1,15 +1,16 @@ from __future__ import annotations +from collections.abc import Iterator from itertools import chain from typing import Any, ClassVar, cast -from attr import define, evolve +from attr import define, evolve, field -from ... import Config +from ... import Config, strings from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonIdentifier from ..errors import ParseError, PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example from .schemas import Schemas @@ -17,18 +18,25 @@ class UnionProperty(PropertyProtocol): """A property representing a Union (anyOf) of other properties""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) inner_properties: list[PropertyProtocol] template: ClassVar[str] = "union_property.py.jinja" @classmethod def build( - cls, *, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str, config: Config + cls, + *, + data: oai.Schema, + name: oai.UntrustedString, + required: bool, + schemas: Schemas, + parent_name: str, + config: Config, ) -> tuple[UnionProperty | PropertyError, Schemas]: """ Create a `UnionProperty` the right way. @@ -45,7 +53,7 @@ def build( `(result, schemas)` where `schemas` is the updated version of the input `schemas` and `result` is the constructed `UnionProperty` or a `PropertyError` describing what went wrong. """ - from . import property_from_data + from . import property_from_data # noqa: PLC0415 sub_properties: list[PropertyProtocol] = [] @@ -55,8 +63,19 @@ def build( type_list_data.append(data.model_copy(update={"type": _type, "default": None})) for i, sub_prop_data in enumerate(chain(data.anyOf, data.oneOf, type_list_data)): + # If a schema has a unique title property, we can use that to carry forward a descriptive name instead of "type_0" + subscript: str + if ( + isinstance(sub_prop_data, oai.Schema) + and sub_prop_data.title is not None + and sub_prop_data.title != data.title + ): + subscript = sub_prop_data.title.get_untrusted_value() + else: + subscript = f"type_{i}" + sub_prop, schemas = property_from_data( - name=f"{name}_type_{i}", + name=oai.UntrustedString(f"{name.get_untrusted_value()}_{subscript}"), required=True, data=sub_prop_data, schemas=schemas, @@ -64,25 +83,32 @@ def build( config=config, ) if isinstance(sub_prop, PropertyError): - return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), schemas + return ( + PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), + schemas, + ) sub_properties.append(sub_prop) - def flatten_union_properties(sub_properties: list[PropertyProtocol]) -> list[PropertyProtocol]: - flattened = [] - for sub_prop in sub_properties: - if isinstance(sub_prop, UnionProperty): - flattened.extend(flatten_union_properties(sub_prop.inner_properties)) + def flatten_union_properties(possibly_nested: list[PropertyProtocol]) -> Iterator[PropertyProtocol]: + for to_flatten in possibly_nested: + if isinstance(to_flatten, UnionProperty): + yield from flatten_union_properties(to_flatten.inner_properties) else: - flattened.append(sub_prop) - return flattened + yield to_flatten - sub_properties = flatten_union_properties(sub_properties) + seen_types = set() + inner_properties: list[PropertyProtocol] = [] + for flattened in flatten_union_properties(sub_properties): + type_string = flattened.get_type_string(no_optional=True) + if type_string not in seen_types: + seen_types.add(type_string) + inner_properties.append(flattened) prop = UnionProperty( name=name, required=required, default=None, - inner_properties=sub_properties, + inner_properties=inner_properties, python_name=PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, @@ -94,7 +120,7 @@ def flatten_union_properties(sub_properties: list[PropertyProtocol]) -> list[Pro prop = evolve(prop, default=default_or_error) return prop, schemas - def convert_value(self, value: Any) -> Value | None | PropertyError: + def convert_value(self, value: Any) -> Value | PropertyError | None: if value is None or isinstance(value, Value): return None value_or_error: Value | PropertyError | None = PropertyError( @@ -106,25 +132,29 @@ def convert_value(self, value: Any) -> Value | None | PropertyError: return value_or_error return value_or_error - def _get_inner_type_strings(self, json: bool, multipart: bool) -> set[str]: + def _get_inner_type_strings(self, json: bool) -> set[strings.PythonCode]: return { - p.get_type_string(no_optional=True, json=json, multipart=multipart, quoted=not p.is_base_type) + p.get_type_string( + no_optional=True, + json=json, + ) for p in self.inner_properties } @staticmethod - def _get_type_string_from_inner_type_strings(inner_types: set[str]) -> str: + def _get_type_string_from_inner_type_strings(inner_types: set[strings.PythonCode]) -> strings.PythonCode: if len(inner_types) == 1: return inner_types.pop() - return f"Union[{', '.join(sorted(inner_types))}]" + joined = " | ".join(sorted((t.as_unembedded_code() for t in inner_types), key=str.lower)) + return strings.PythonCode(joined) - def get_base_type_string(self, *, quoted: bool = False) -> str: - return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=False, multipart=False)) + def get_base_type_string(self) -> strings.PythonCode: + return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=False)) - def get_base_json_type_string(self, *, quoted: bool = False) -> str: - return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=True, multipart=False)) + def get_base_json_type_string(self) -> strings.PythonCode: + return self._get_type_string_from_inner_type_strings(self._get_inner_type_strings(json=True)) - def get_type_strings_in_union(self, *, no_optional: bool = False, json: bool, multipart: bool) -> set[str]: + def get_type_strings_in_union(self, *, no_optional: bool = False, json: bool) -> set[strings.PythonCode]: """ Get the set of all the types that should appear within the `Union` representing this property. @@ -133,32 +163,28 @@ def get_type_strings_in_union(self, *, no_optional: bool = False, json: bool, mu Args: no_optional: Do not include `None` or `Unset` in this set. json: If True, this returns the JSON types, not the Python types, of this property. - multipart: If True, this returns the multipart types, not the Python types, of this property. Returns: - A set of strings containing the types that should appear within `Union`. + A set of code snippets containing the types that should appear within `Union`. """ - type_strings = self._get_inner_type_strings(json=json, multipart=multipart) + type_strings = self._get_inner_type_strings(json=json) if no_optional: return type_strings if not self.required: - type_strings.add("Unset") + type_strings.add(strings.PythonCode("Unset")) return type_strings def get_type_string( self, no_optional: bool = False, json: bool = False, - *, - multipart: bool = False, - quoted: bool = False, - ) -> str: + ) -> strings.PythonCode: """ - Get a string representation of type that should be used when declaring this property. + Get a Python code representation of type that should be used when declaring this property. This implementation differs slightly from `Property.get_type_string` in order to collapse nested union types. """ - type_strings_in_union = self.get_type_strings_in_union(no_optional=no_optional, json=json, multipart=multipart) + type_strings_in_union = self.get_type_strings_in_union(no_optional=no_optional, json=json) return self._get_type_string_from_inner_type_strings(type_strings_in_union) def get_imports(self, *, prefix: str) -> set[str]: @@ -172,7 +198,7 @@ def get_imports(self, *, prefix: str) -> set[str]: imports = super().get_imports(prefix=prefix) for inner_prop in self.inner_properties: imports.update(inner_prop.get_imports(prefix=prefix)) - imports.add("from typing import cast, Union") + imports.add("from typing import cast") return imports def get_lazy_imports(self, *, prefix: str) -> set[str]: @@ -183,9 +209,10 @@ def get_lazy_imports(self, *, prefix: str) -> set[str]: def validate_location(self, location: oai.ParameterLocation) -> ParseError | None: """Returns an error if this type of property is not allowed in the given location""" - from ..properties import Property + from ..properties import Property # noqa: PLC0415 for inner_prop in self.inner_properties: if evolve(cast(Property, inner_prop), required=self.required).validate_location(location) is not None: - return ParseError(detail=f"{self.get_type_string()} is not allowed in {location}") + type_string = self.get_type_string().as_unembedded_code() + return ParseError(detail=f"{type_string} is not allowed in {location}") return None diff --git a/openapi_python_client/parser/properties/uuid.py b/openapi_python_client/parser/properties/uuid.py index 86d7d6a0a..ead85f188 100644 --- a/openapi_python_client/parser/properties/uuid.py +++ b/openapi_python_client/parser/properties/uuid.py @@ -3,24 +3,24 @@ from typing import Any, ClassVar from uuid import UUID -from attr import define +from attr import define, field from ... import schema as oai -from ...utils import PythonIdentifier +from ...strings import PythonCode, PythonIdentifier from ..errors import PropertyError -from .protocol import PropertyProtocol, Value +from .protocol import PropertyProtocol, Value, convert_example @define class UuidProperty(PropertyProtocol): """A property of type uuid.UUID""" - name: str + name: oai.UntrustedString required: bool default: Value | None python_name: PythonIdentifier - description: str | None - example: str | None + description: oai.UntrustedString | None + example: oai.UntrustedString | None = field(converter=convert_example) _type_string: ClassVar[str] = "UUID" _json_type_string: ClassVar[str] = "str" @@ -35,12 +35,13 @@ class UuidProperty(PropertyProtocol): @classmethod def build( cls, - name: str, + *, + name: oai.UntrustedString, required: bool, default: Any, python_name: PythonIdentifier, - description: str | None, - example: str | None, + description: oai.UntrustedString | None, + example: Any, ) -> UuidProperty | PropertyError: checked_default = cls.convert_value(default) if isinstance(checked_default, PropertyError): @@ -56,7 +57,7 @@ def build( ) @classmethod - def convert_value(cls, value: Any) -> Value | None | PropertyError: + def convert_value(cls, value: Any) -> Value | PropertyError | None: if value is None or isinstance(value, Value): return value if isinstance(value, str): @@ -64,7 +65,7 @@ def convert_value(cls, value: Any) -> Value | None | PropertyError: UUID(value) except ValueError: return PropertyError(f"Invalid UUID value: {value}") - return Value(python_code=f"UUID('{value}')", raw_value=value) + return Value(python_code=PythonCode(f"UUID('{value}')"), raw_value=value) return PropertyError(f"Invalid UUID value: {value}") def get_imports(self, *, prefix: str) -> set[str]: diff --git a/openapi_python_client/parser/responses.py b/openapi_python_client/parser/responses.py index d313f81ad..60311aae1 100644 --- a/openapi_python_client/parser/responses.py +++ b/openapi_python_client/parser/responses.py @@ -1,21 +1,37 @@ -__all__ = ["Response", "response_from_data"] +__all__ = ["HTTPStatusPattern", "Response", "Responses", "response_from_data"] -from http import HTTPStatus -from typing import Optional, TypedDict, Union +from collections.abc import Iterator +from typing import TypedDict from attrs import define -from openapi_python_client import utils +from openapi_python_client import strings +from openapi_python_client.parser.properties.schemas import get_reference_simple_name, parse_reference_path from .. import Config from .. import schema as oai -from ..utils import PythonIdentifier +from ..schema.untrusted_string import UntrustedString +from ..strings import PythonIdentifier from .errors import ParseError, PropertyError from .properties import AnyProperty, Property, Schemas, property_from_data +@define +class Responses: + patterns: list["Response"] + default: "Response | None" + + def __iter__(self) -> Iterator["Response"]: + yield from self.patterns + if self.default: + yield self.default + + def __len__(self) -> int: + return len(self.patterns) + (1 if self.default else 0) + + class _ResponseSource(TypedDict): - """What data should be pulled from the httpx Response object""" + """What data should be pulled from the httpx2 Response object""" attribute: str return_type: str @@ -27,18 +43,95 @@ class _ResponseSource(TypedDict): NONE_SOURCE = _ResponseSource(attribute="None", return_type="None") -@define +class HTTPStatusPattern: + """Status code patterns come in three flavors, in order of precedence: + 1. Specific status codes, such as 200. This is represented by `min` and `max` being the same. + 2. Ranges of status codes, such as 2XX. This is represented by `min` and `max` being different. + 3. The special `default` status code, which is used when no other status codes match. `range` is `None` in this case. + + https://github.com/openapi-generators/openapi-python-client/blob/61b6c54994e2a6285bb422ee3b864c45b5d88c15/openapi_python_client/schema/3.1.0.md#responses-object + """ + + pattern: str + range: tuple[int, int] | None + + def __init__(self, *, pattern: str, code_range: tuple[int, int] | None): + """Initialize with a range of status codes or None for the default case.""" + self.pattern = pattern + self.range = code_range + + @staticmethod + def parse(pattern: UntrustedString) -> "HTTPStatusPattern | ParseError": + """Parse a status code pattern such as 2XX or 404""" + untrusted = pattern.get_untrusted_value() + if untrusted == "default": + return HTTPStatusPattern(pattern=untrusted, code_range=None) + + if len(untrusted) == 3 and untrusted.endswith("XX") and untrusted[0].isdigit(): + first_digit = int(untrusted[0]) + return HTTPStatusPattern(pattern=untrusted, code_range=(first_digit * 100, first_digit * 100 + 99)) + + try: + code = int(untrusted) + return HTTPStatusPattern(pattern=untrusted, code_range=(code, code)) + except ValueError: + return ParseError( + detail=( + f"Invalid response status code pattern: {untrusted}, response will be omitted from generated client" + ) + ) + + def is_range(self) -> bool: + """Check if this is a range of status codes, such as 2XX""" + return self.range is not None and self.range[0] != self.range[1] + + def __lt__(self, other: "HTTPStatusPattern") -> bool: + """Compare two HTTPStatusPattern objects based on the order they should be applied in""" + if self.range is None: + return False # Default gets applied last + if other.range is None: + return True # Other is default, so this one gets applied first + + # Specific codes appear before ranges + if self.is_range() and not other.is_range(): + return False + if not self.is_range() and other.is_range(): + return True + + # Order specific codes numerically + return self.range[0] < other.range[0] + + def __eq__(self, other: object) -> bool: # pragma: no cover + if not isinstance(other, HTTPStatusPattern): + return False + return self.range == other.range + + def __hash__(self) -> int: # pragma: no cover + return hash(self.range) + + def __repr__(self) -> str: # pragma: no cover + return f"" + + +@define(order=False) class Response: """Describes a single response for an endpoint""" - status_code: HTTPStatus + status_code: HTTPStatusPattern prop: Property source: _ResponseSource - data: Union[oai.Response, oai.Reference] # Original data which created this response, useful for custom templates + data: oai.Response | oai.Reference # Original data which created this response, useful for custom templates + + def is_default(self) -> bool: + return self.status_code.range is None + + def __lt__(self, other: "Response") -> bool: + """Compare two responses based on the order in which they should be applied in""" + return self.status_code < other.status_code -def _source_by_content_type(content_type: str, config: Config) -> Optional[_ResponseSource]: - parsed_content_type = utils.get_content_type(content_type, config) +def _source_by_content_type(content_type: str, config: Config) -> _ResponseSource | None: + parsed_content_type = strings.get_content_type(content_type, config) if parsed_content_type is None: return None @@ -58,10 +151,10 @@ def _source_by_content_type(content_type: str, config: Config) -> Optional[_Resp def empty_response( *, - status_code: HTTPStatus, - response_name: str, + status_code: HTTPStatusPattern, + response_name: UntrustedString, config: Config, - data: Union[oai.Response, oai.Reference], + data: oai.Response | oai.Reference, ) -> Response: """Return an untyped response, for when no response type is defined""" return Response( @@ -79,27 +172,32 @@ def empty_response( ) -def response_from_data( +def response_from_data( # noqa: PLR0911 *, - status_code: HTTPStatus, - data: Union[oai.Response, oai.Reference], + status_code: HTTPStatusPattern, + data: oai.Response | oai.Reference, schemas: Schemas, + responses: dict[UntrustedString, oai.Response | oai.Reference], parent_name: str, config: Config, -) -> tuple[Union[Response, ParseError], Schemas]: +) -> tuple[Response | ParseError, Schemas]: """Generate a Response from the OpenAPI dictionary representation of it""" - response_name = f"response_{status_code}" + response_name = UntrustedString(f"response_{status_code.pattern}") if isinstance(data, oai.Reference): - return ( - empty_response( - status_code=status_code, - response_name=response_name, - config=config, - data=data, - ), - schemas, - ) + ref_path = parse_reference_path(data.ref) + if isinstance(ref_path, ParseError): # pragma: no cover + return ref_path, schemas + if not ref_path.startswith("/components/responses/"): # pragma: no cover + return ParseError( + data=data, detail=f"$ref to {data.ref.get_untrusted_value()} not allowed in responses" + ), schemas + resp_data = responses.get(get_reference_simple_name(ref_path), None) + if not resp_data: # pragma: no cover + return ParseError(data=data, detail=f"Could not find reference: {data.ref.get_untrusted_value()}"), schemas + if not isinstance(resp_data, oai.Response): # pragma: no cover + return ParseError(data=data, detail="Top-level $ref inside components/responses is not supported"), schemas + data = resp_data content = data.content if not content: @@ -114,13 +212,16 @@ def response_from_data( ) for content_type, media_type in content.items(): - source = _source_by_content_type(content_type, config) + source = _source_by_content_type(content_type.get_untrusted_value(), config) if source is not None: schema_data = media_type.media_type_schema break else: return ( - ParseError(data=data, detail=f"Unsupported content_type {content}"), + ParseError( + data=data, + detail=f"Unsupported content_type {', '.join(key.get_untrusted_value() for key in content)}", + ), schemas, ) diff --git a/openapi_python_client/schema/__init__.py b/openapi_python_client/schema/__init__.py index 21a90f5fb..41ed92cf2 100644 --- a/openapi_python_client/schema/__init__.py +++ b/openapi_python_client/schema/__init__.py @@ -4,7 +4,6 @@ "OpenAPI", "Operation", "Parameter", - "Parameter", "ParameterLocation", "PathItem", "Reference", @@ -12,6 +11,7 @@ "Response", "Responses", "Schema", + "UntrustedString", ] @@ -29,3 +29,4 @@ Schema, ) from .parameter_location import ParameterLocation +from .untrusted_string import UntrustedString diff --git a/openapi_python_client/schema/data_type.py b/openapi_python_client/schema/data_type.py index 1c104142e..e80f39803 100644 --- a/openapi_python_client/schema/data_type.py +++ b/openapi_python_client/schema/data_type.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class DataType(str, Enum): +class DataType(StrEnum): """The data type of a schema is defined by the type keyword References: diff --git a/openapi_python_client/schema/openapi_schema_pydantic/callback.py b/openapi_python_client/schema/openapi_schema_pydantic/callback.py index f4593cc8d..ddf6a3434 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/callback.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/callback.py @@ -1,11 +1,13 @@ from typing import TYPE_CHECKING +from ..untrusted_string import UntrustedString + if TYPE_CHECKING: # pragma: no cover from .path_item import PathItem else: PathItem = "PathItem" -Callback = dict[str, PathItem] +Callback = dict[UntrustedString, PathItem] """ A map of possible out-of band callbacks related to the parent operation. Each value in the map is a [Path Item Object](#pathItemObject) diff --git a/openapi_python_client/schema/openapi_schema_pydantic/components.py b/openapi_python_client/schema/openapi_schema_pydantic/components.py index babe26265..8423247d7 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/components.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/components.py @@ -1,13 +1,12 @@ -from typing import Optional, Union - from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .callback import Callback from .example import Example from .header import Header from .link import Link from .parameter import Parameter -from .reference import Reference +from .reference import ReferenceOr from .request_body import RequestBody from .response import Response from .schema import Schema @@ -25,15 +24,15 @@ class Components(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject """ - schemas: Optional[dict[str, Union[Schema, Reference]]] = None - responses: Optional[dict[str, Union[Response, Reference]]] = None - parameters: Optional[dict[str, Union[Parameter, Reference]]] = None - examples: Optional[dict[str, Union[Example, Reference]]] = None - requestBodies: Optional[dict[str, Union[RequestBody, Reference]]] = None - headers: Optional[dict[str, Union[Header, Reference]]] = None - securitySchemes: Optional[dict[str, Union[SecurityScheme, Reference]]] = None - links: Optional[dict[str, Union[Link, Reference]]] = None - callbacks: Optional[dict[str, Union[Callback, Reference]]] = None + schemas: dict[UntrustedString, ReferenceOr[Schema]] | None = None + responses: dict[UntrustedString, ReferenceOr[Response]] | None = None + parameters: dict[UntrustedString, ReferenceOr[Parameter]] | None = None + examples: dict[UntrustedString, ReferenceOr[Example]] | None = None + requestBodies: dict[UntrustedString, ReferenceOr[RequestBody]] | None = None + headers: dict[UntrustedString, ReferenceOr[Header]] | None = None + securitySchemes: dict[UntrustedString, ReferenceOr[SecurityScheme]] | None = None + links: dict[UntrustedString, ReferenceOr[Link]] | None = None + callbacks: dict[UntrustedString, ReferenceOr[Callback]] | None = None model_config = ConfigDict( # `Callback` contains an unresolvable forward reference, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/contact.py b/openapi_python_client/schema/openapi_schema_pydantic/contact.py index c04fdbbe0..ea7cbe33d 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/contact.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/contact.py @@ -1,7 +1,7 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict +from openapi_python_client.schema.untrusted_string import UntrustedString + class Contact(BaseModel): """ @@ -11,9 +11,9 @@ class Contact(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contactObject """ - name: Optional[str] = None - url: Optional[str] = None - email: Optional[str] = None + name: UntrustedString | None = None + url: UntrustedString | None = None + email: UntrustedString | None = None model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py b/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py index 9f36773ba..3a10ce7f3 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/discriminator.py @@ -1,7 +1,7 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class Discriminator(BaseModel): """ @@ -18,8 +18,8 @@ class Discriminator(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminatorObject """ - propertyName: str - mapping: Optional[dict[str, str]] = None + propertyName: UntrustedString + mapping: dict[UntrustedString, UntrustedString] | None = None model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/encoding.py b/openapi_python_client/schema/openapi_schema_pydantic/encoding.py index ebf6295dc..f09a7602e 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/encoding.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/encoding.py @@ -1,8 +1,9 @@ -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING from pydantic import BaseModel, ConfigDict -from .reference import Reference +from ..untrusted_string import UntrustedString +from .reference import ReferenceOr if TYPE_CHECKING: # pragma: no cover from .header import Header @@ -16,9 +17,9 @@ class Encoding(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encodingObject """ - contentType: Optional[str] = None - headers: Optional[dict[str, Union["Header", Reference]]] = None - style: Optional[str] = None + contentType: UntrustedString | None = None + headers: dict[UntrustedString, ReferenceOr["Header"]] | None = None + style: UntrustedString | None = None explode: bool = False allowReserved: bool = False model_config = ConfigDict( diff --git a/openapi_python_client/schema/openapi_schema_pydantic/example.py b/openapi_python_client/schema/openapi_schema_pydantic/example.py index 90db2530e..bd3d188aa 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/example.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/example.py @@ -1,7 +1,9 @@ -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class Example(BaseModel): """Examples added to parameters / components to help clarify usage. @@ -11,10 +13,10 @@ class Example(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#exampleObject """ - summary: Optional[str] = None - description: Optional[str] = None - value: Optional[Any] = None - externalValue: Optional[str] = None + summary: UntrustedString | None = None + description: UntrustedString | None = None + value: Any | None = None + externalValue: UntrustedString | None = None model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py b/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py index 2c0c39b7c..b5565beb8 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py @@ -1,7 +1,7 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict +from openapi_python_client.schema.untrusted_string import UntrustedString + class ExternalDocumentation(BaseModel): """Allows referencing an external resource for extended documentation. @@ -10,8 +10,8 @@ class ExternalDocumentation(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#externalDocumentationObject """ - description: Optional[str] = None - url: str + description: UntrustedString | None = None + url: UntrustedString model_config = ConfigDict( extra="allow", json_schema_extra={"examples": [{"description": "Find more info here", "url": "https://example.com"}]}, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/header.py b/openapi_python_client/schema/openapi_schema_pydantic/header.py index 2deb6f390..e81300459 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/header.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/header.py @@ -1,10 +1,15 @@ -from pydantic import ConfigDict, Field +from typing import Any -from ..parameter_location import ParameterLocation -from .parameter import Parameter +from pydantic import BaseModel, ConfigDict, Field +from ..untrusted_string import UntrustedString +from .example import Example +from .media_type import MediaType +from .reference import ReferenceOr +from .schema import Schema -class Header(Parameter): + +class Header(BaseModel): """ The Header Object follows the structure of the [Parameter Object](#parameterObject) with the following changes: @@ -18,8 +23,17 @@ class Header(Parameter): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#headerObject """ - name: str = Field(default="") - param_in: ParameterLocation = Field(default=ParameterLocation.HEADER, alias="in") + description: UntrustedString | None = None + required: bool = False + deprecated: bool = False + allowEmptyValue: bool = False + style: UntrustedString | None = None + explode: bool = False + allowReserved: bool = False + param_schema: ReferenceOr[Schema] | None = Field(default=None, alias="schema") + example: Any | None = None + examples: dict[UntrustedString, ReferenceOr[Example]] | None = None + content: dict[UntrustedString, MediaType] | None = None model_config = ConfigDict( # `Parameter` is not build yet, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/info.py b/openapi_python_client/schema/openapi_schema_pydantic/info.py index bec1354da..316ed538e 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/info.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/info.py @@ -1,7 +1,6 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .contact import Contact from .license import License @@ -17,12 +16,12 @@ class Info(BaseModel): -https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#infoObject """ - title: str - description: Optional[str] = None - termsOfService: Optional[str] = None - contact: Optional[Contact] = None - license: Optional[License] = None - version: str + title: UntrustedString + description: UntrustedString | None = None + termsOfService: UntrustedString | None = None + contact: Contact | None = None + license: License | None = None + version: UntrustedString model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/license.py b/openapi_python_client/schema/openapi_schema_pydantic/license.py index 185eec1db..1c40b285a 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/license.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/license.py @@ -1,7 +1,7 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class License(BaseModel): """ @@ -11,8 +11,8 @@ class License(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#licenseObject """ - name: str - url: Optional[str] = None + name: UntrustedString + url: UntrustedString | None = None model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/link.py b/openapi_python_client/schema/openapi_schema_pydantic/link.py index 69cdf29c0..feccecccc 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/link.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/link.py @@ -1,7 +1,8 @@ -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .server import Server @@ -23,12 +24,12 @@ class Link(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#linkObject """ - operationRef: Optional[str] = None - operationId: Optional[str] = None - parameters: Optional[dict[str, Any]] = None - requestBody: Optional[Any] = None - description: Optional[str] = None - server: Optional[Server] = None + operationRef: UntrustedString | None = None + operationId: UntrustedString | None = None + parameters: dict[UntrustedString, Any] | None = None + requestBody: Any | None = None + description: UntrustedString | None = None + server: Server | None = None model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/media_type.py b/openapi_python_client/schema/openapi_schema_pydantic/media_type.py index 95f9ede14..7418b9715 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/media_type.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/media_type.py @@ -1,10 +1,11 @@ -from typing import Any, Optional, Union +from typing import Any from pydantic import BaseModel, ConfigDict, Field +from ..untrusted_string import UntrustedString from .encoding import Encoding from .example import Example -from .reference import Reference +from .reference import ReferenceOr from .schema import Schema @@ -16,10 +17,10 @@ class MediaType(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#mediaTypeObject """ - media_type_schema: Optional[Union[Reference, Schema]] = Field(default=None, alias="schema") - example: Optional[Any] = None - examples: Optional[dict[str, Union[Example, Reference]]] = None - encoding: Optional[dict[str, Encoding]] = None + media_type_schema: ReferenceOr[Schema] | None = Field(default=None, alias="schema") + example: Any | None = None + examples: dict[UntrustedString, ReferenceOr[Example]] | None = None + encoding: dict[UntrustedString, Encoding] | None = None model_config = ConfigDict( # `Encoding` is not build yet, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py b/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py index 16e366090..8320c148a 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py @@ -1,7 +1,7 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class OAuthFlow(BaseModel): """ @@ -12,10 +12,10 @@ class OAuthFlow(BaseModel): - https://swagger.io/docs/specification/authentication/oauth2/ """ - authorizationUrl: Optional[str] = None - tokenUrl: Optional[str] = None - refreshUrl: Optional[str] = None - scopes: dict[str, str] + authorizationUrl: UntrustedString | None = None + tokenUrl: UntrustedString | None = None + refreshUrl: UntrustedString | None = None + scopes: dict[UntrustedString, UntrustedString] model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py b/openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py index dba193713..602a34cb5 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/oauth_flows.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict from .oauth_flow import OAuthFlow @@ -14,8 +12,8 @@ class OAuthFlows(BaseModel): - https://swagger.io/docs/specification/authentication/oauth2/ """ - implicit: Optional[OAuthFlow] = None - password: Optional[OAuthFlow] = None - clientCredentials: Optional[OAuthFlow] = None - authorizationCode: Optional[OAuthFlow] = None + implicit: OAuthFlow | None = None + password: OAuthFlow | None = None + clientCredentials: OAuthFlow | None = None + authorizationCode: OAuthFlow | None = None model_config = ConfigDict(extra="allow") diff --git a/openapi_python_client/schema/openapi_schema_pydantic/open_api.py b/openapi_python_client/schema/openapi_schema_pydantic/open_api.py index e66ea942c..890cf5793 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/open_api.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/open_api.py @@ -1,7 +1,6 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict, field_validator +from ..untrusted_string import UntrustedString from .components import Components from .external_documentation import ExternalDocumentation from .info import Info @@ -24,11 +23,11 @@ class OpenAPI(BaseModel): info: Info servers: list[Server] = [Server(url="/")] paths: Paths - components: Optional[Components] = None - security: Optional[list[SecurityRequirement]] = None - tags: Optional[list[Tag]] = None - externalDocs: Optional[ExternalDocumentation] = None - openapi: str + components: Components | None = None + security: list[SecurityRequirement] | None = None + tags: list[Tag] | None = None + externalDocs: ExternalDocumentation | None = None + openapi: UntrustedString model_config = ConfigDict( # `Components` is not build yet, will rebuild in `__init__.py`: defer_build=True, @@ -37,8 +36,9 @@ class OpenAPI(BaseModel): @field_validator("openapi") @classmethod - def check_openapi_version(cls, value: str) -> str: + def check_openapi_version(cls, untrusted: UntrustedString) -> UntrustedString: """Validates that the declared OpenAPI version is a supported one""" + value = untrusted.get_untrusted_value() parts = value.split(".") if len(parts) != NUM_SEMVER_PARTS: raise ValueError(f"Invalid OpenAPI version {value}") @@ -46,4 +46,4 @@ def check_openapi_version(cls, value: str) -> str: raise ValueError(f"Only OpenAPI versions 3.* are supported, got {value}") if int(parts[1]) > 1: raise ValueError(f"Only OpenAPI versions 3.1.* are supported, got {value}") - return value + return untrusted diff --git a/openapi_python_client/schema/openapi_schema_pydantic/operation.py b/openapi_python_client/schema/openapi_schema_pydantic/operation.py index ebf5e1faa..9bc1dbef3 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/operation.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/operation.py @@ -1,11 +1,10 @@ -from typing import Optional, Union - from pydantic import BaseModel, ConfigDict, Field +from ..untrusted_string import UntrustedString from .callback import Callback from .external_documentation import ExternalDocumentation from .parameter import Parameter -from .reference import Reference +from .reference import ReferenceOr from .request_body import RequestBody from .responses import Responses from .security_requirement import SecurityRequirement @@ -20,19 +19,19 @@ class Operation(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operationObject """ - tags: Optional[list[str]] = None - summary: Optional[str] = None - description: Optional[str] = None - externalDocs: Optional[ExternalDocumentation] = None - operationId: Optional[str] = None - parameters: Optional[list[Union[Parameter, Reference]]] = None - request_body: Optional[Union[RequestBody, Reference]] = Field(None, alias="requestBody") + tags: list[UntrustedString] | None = None + summary: UntrustedString | None = None + description: UntrustedString | None = None + externalDocs: ExternalDocumentation | None = None + operationId: UntrustedString | None = None + parameters: list[ReferenceOr[Parameter]] | None = None + request_body: ReferenceOr[RequestBody] | None = Field(None, alias="requestBody") responses: Responses - callbacks: Optional[dict[str, Callback]] = None + callbacks: dict[UntrustedString, Callback] | None = None deprecated: bool = False - security: Optional[list[SecurityRequirement]] = None - servers: Optional[list[Server]] = None + security: list[SecurityRequirement] | None = None + servers: list[Server] | None = None model_config = ConfigDict( # `Callback` contains an unresolvable forward reference, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/parameter.py b/openapi_python_client/schema/openapi_schema_pydantic/parameter.py index 6f6fe9342..880cc6560 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/parameter.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/parameter.py @@ -1,11 +1,12 @@ -from typing import Any, Optional, Union +from typing import Any from pydantic import BaseModel, ConfigDict, Field from ..parameter_location import ParameterLocation +from ..untrusted_string import UntrustedString from .example import Example from .media_type import MediaType -from .reference import Reference +from .reference import ReferenceOr from .schema import Schema @@ -21,19 +22,19 @@ class Parameter(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameterObject """ - name: str + name: UntrustedString param_in: ParameterLocation = Field(alias="in") - description: Optional[str] = None + description: UntrustedString | None = None required: bool = False deprecated: bool = False allowEmptyValue: bool = False - style: Optional[str] = None + style: UntrustedString | None = None explode: bool = False allowReserved: bool = False - param_schema: Optional[Union[Reference, Schema]] = Field(default=None, alias="schema") - example: Optional[Any] = None - examples: Optional[dict[str, Union[Example, Reference]]] = None - content: Optional[dict[str, MediaType]] = None + param_schema: ReferenceOr[Schema] | None = Field(default=None, alias="schema") + example: Any | None = None + examples: dict[UntrustedString, ReferenceOr[Example]] | None = None + content: dict[UntrustedString, MediaType] | None = None model_config = ConfigDict( # `MediaType` is not build yet, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/path_item.py b/openapi_python_client/schema/openapi_schema_pydantic/path_item.py index 8c1eab6ea..5e1116f3d 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/path_item.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/path_item.py @@ -1,9 +1,11 @@ -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING from pydantic import BaseModel, ConfigDict, Field +from ..ref import Ref +from ..untrusted_string import UntrustedString from .parameter import Parameter -from .reference import Reference +from .reference import ReferenceOr from .server import Server if TYPE_CHECKING: @@ -22,19 +24,19 @@ class PathItem(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#pathItemObject """ - ref: Optional[str] = Field(default=None, alias="$ref") - summary: Optional[str] = None - description: Optional[str] = None - get: Optional["Operation"] = None - put: Optional["Operation"] = None - post: Optional["Operation"] = None - delete: Optional["Operation"] = None - options: Optional["Operation"] = None - head: Optional["Operation"] = None - patch: Optional["Operation"] = None - trace: Optional["Operation"] = None - servers: Optional[list[Server]] = None - parameters: Optional[list[Union[Parameter, Reference]]] = None + ref: Ref | None = Field(default=None, alias="$ref") + summary: UntrustedString | None = None + description: UntrustedString | None = None + get: "Operation | None" = None + put: "Operation | None" = None + post: "Operation | None" = None + delete: "Operation | None" = None + options: "Operation | None" = None + head: "Operation | None" = None + patch: "Operation | None" = None + trace: "Operation | None" = None + servers: list[Server] | None = None + parameters: list[ReferenceOr[Parameter]] | None = None model_config = ConfigDict( # `Operation` is an unresolvable forward reference, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/paths.py b/openapi_python_client/schema/openapi_schema_pydantic/paths.py index 86c1dfd19..bc7181edf 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/paths.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/paths.py @@ -1,6 +1,7 @@ +from ..untrusted_string import UntrustedString from .path_item import PathItem -Paths = dict[str, PathItem] +Paths = dict[UntrustedString, PathItem] """ Holds the relative paths to the individual endpoints and their operations. The path is appended to the URL from the [`Server Object`](#serverObject) in order to construct the full URL. diff --git a/openapi_python_client/schema/openapi_schema_pydantic/reference.py b/openapi_python_client/schema/openapi_schema_pydantic/reference.py index 50d26064f..c62a7538d 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/reference.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/reference.py @@ -1,4 +1,8 @@ -from pydantic import BaseModel, ConfigDict, Field +from typing import Annotated, Any, Literal, TypeAlias, TypeVar + +from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag + +from openapi_python_client.schema.ref import Ref class Reference(BaseModel): @@ -16,7 +20,7 @@ class Reference(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#referenceObject """ - ref: str = Field(alias="$ref") + ref: Ref = Field(alias="$ref") model_config = ConfigDict( extra="allow", populate_by_name=True, @@ -24,3 +28,17 @@ class Reference(BaseModel): "examples": [{"$ref": "#/components/schemas/Pet"}, {"$ref": "Pet.json"}, {"$ref": "definitions.json#/Pet"}] }, ) + + +T = TypeVar("T") + + +def _reference_discriminator(obj: Any) -> Literal["ref", "other"]: + if isinstance(obj, dict): + return "ref" if "$ref" in obj else "other" + return "ref" if isinstance(obj, Reference) else "other" + + +ReferenceOr: TypeAlias = Annotated[ + Annotated[Reference, Tag("ref")] | Annotated[T, Tag("other")], Discriminator(_reference_discriminator) +] diff --git a/openapi_python_client/schema/openapi_schema_pydantic/request_body.py b/openapi_python_client/schema/openapi_schema_pydantic/request_body.py index 8cd9bb527..361ac8102 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/request_body.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/request_body.py @@ -1,7 +1,6 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .media_type import MediaType @@ -13,8 +12,8 @@ class RequestBody(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#requestBodyObject """ - description: Optional[str] = None - content: dict[str, MediaType] + description: UntrustedString | None = None + content: dict[UntrustedString, MediaType] required: bool = False model_config = ConfigDict( # `MediaType` is not build yet, will rebuild in `__init__.py`: diff --git a/openapi_python_client/schema/openapi_schema_pydantic/response.py b/openapi_python_client/schema/openapi_schema_pydantic/response.py index b7ec0d357..ae972c16c 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/response.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/response.py @@ -1,11 +1,10 @@ -from typing import Optional, Union - from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .header import Header from .link import Link from .media_type import MediaType -from .reference import Reference +from .reference import ReferenceOr class Response(BaseModel): @@ -18,10 +17,10 @@ class Response(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responseObject """ - description: str - headers: Optional[dict[str, Union[Header, Reference]]] = None - content: Optional[dict[str, MediaType]] = None - links: Optional[dict[str, Union[Link, Reference]]] = None + description: UntrustedString + headers: dict[UntrustedString, ReferenceOr[Header]] | None = None + content: dict[UntrustedString, MediaType] | None = None + links: dict[UntrustedString, ReferenceOr[Link]] | None = None model_config = ConfigDict( # `MediaType` is not build yet, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/responses.py b/openapi_python_client/schema/openapi_schema_pydantic/responses.py index 17ddc13fe..9714bd535 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/responses.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/responses.py @@ -1,9 +1,8 @@ -from typing import Union - -from .reference import Reference +from ..untrusted_string import UntrustedString +from .reference import ReferenceOr from .response import Response -Responses = dict[str, Union[Response, Reference]] +Responses = dict[UntrustedString, ReferenceOr[Response]] """ A container for the expected responses of an operation. The container maps a HTTP response code to the expected response. diff --git a/openapi_python_client/schema/openapi_schema_pydantic/schema.py b/openapi_python_client/schema/openapi_schema_pydantic/schema.py index 99c64eb51..3fd90bfdd 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/schema.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/schema.py @@ -1,11 +1,12 @@ -from typing import Any, Optional, Union +from typing import Any from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, model_validator from ..data_type import DataType +from ..untrusted_string import UntrustedString from .discriminator import Discriminator from .external_documentation import ExternalDocumentation -from .reference import Reference +from .reference import ReferenceOr from .xml import XML @@ -20,43 +21,43 @@ class Schema(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schemaObject """ - title: Optional[str] = None - multipleOf: Optional[float] = Field(default=None, gt=0.0) - maximum: Optional[float] = None - exclusiveMaximum: Optional[Union[bool, float]] = None - minimum: Optional[float] = None - exclusiveMinimum: Optional[Union[bool, float]] = None - maxLength: Optional[int] = Field(default=None, ge=0) - minLength: Optional[int] = Field(default=None, ge=0) - pattern: Optional[str] = None - maxItems: Optional[int] = Field(default=None, ge=0) - minItems: Optional[int] = Field(default=None, ge=0) - uniqueItems: Optional[bool] = None - maxProperties: Optional[int] = Field(default=None, ge=0) - minProperties: Optional[int] = Field(default=None, ge=0) - required: Optional[list[str]] = Field(default=None) - enum: Union[None, list[Any]] = Field(default=None, min_length=1) - const: Union[None, StrictStr, StrictInt, StrictFloat, StrictBool] = None - type: Union[DataType, list[DataType], None] = Field(default=None) - allOf: list[Union[Reference, "Schema"]] = Field(default_factory=list) - oneOf: list[Union[Reference, "Schema"]] = Field(default_factory=list) - anyOf: list[Union[Reference, "Schema"]] = Field(default_factory=list) - schema_not: Optional[Union[Reference, "Schema"]] = Field(default=None, alias="not") - items: Optional[Union[Reference, "Schema"]] = None - prefixItems: list[Union[Reference, "Schema"]] = Field(default_factory=list) - properties: Optional[dict[str, Union[Reference, "Schema"]]] = None - additionalProperties: Optional[Union[bool, Reference, "Schema"]] = None - description: Optional[str] = None - schema_format: Optional[str] = Field(default=None, alias="format") - default: Optional[Any] = None + title: UntrustedString | None = None + multipleOf: float | None = Field(default=None, gt=0.0) + maximum: float | None = None + exclusiveMaximum: bool | float | None = None + minimum: float | None = None + exclusiveMinimum: bool | float | None = None + maxLength: int | None = Field(default=None, ge=0) + minLength: int | None = Field(default=None, ge=0) + pattern: UntrustedString | None = None + maxItems: int | None = Field(default=None, ge=0) + minItems: int | None = Field(default=None, ge=0) + uniqueItems: bool | None = None + maxProperties: int | None = Field(default=None, ge=0) + minProperties: int | None = Field(default=None, ge=0) + required: list[UntrustedString] | None = Field(default=None) + enum: list[Any] | None = Field(default=None, min_length=1) + const: StrictStr | StrictInt | StrictFloat | StrictBool | None = None + type: DataType | list[DataType] | None = Field(default=None) + allOf: list[ReferenceOr["Schema"]] = Field(default_factory=list) + oneOf: list[ReferenceOr["Schema"]] = Field(default_factory=list) + anyOf: list[ReferenceOr["Schema"]] = Field(default_factory=list) + schema_not: ReferenceOr["Schema"] | None = Field(default=None, alias="not") + items: ReferenceOr["Schema"] | None = None + prefixItems: list[ReferenceOr["Schema"]] = Field(default_factory=list) + properties: dict[UntrustedString, ReferenceOr["Schema"]] | None = None + additionalProperties: bool | ReferenceOr["Schema"] | None = None + description: UntrustedString | None = None + schema_format: UntrustedString | None = Field(default=None, alias="format") + default: Any | None = None nullable: bool = Field(default=False) - discriminator: Optional[Discriminator] = None - readOnly: Optional[bool] = None - writeOnly: Optional[bool] = None - xml: Optional[XML] = None - externalDocs: Optional[ExternalDocumentation] = None - example: Optional[Any] = None - deprecated: Optional[bool] = None + discriminator: Discriminator | None = None + readOnly: bool | None = None + writeOnly: bool | None = None + xml: XML | None = None + externalDocs: ExternalDocumentation | None = None + example: Any | None = None + deprecated: bool | None = None model_config = ConfigDict( extra="allow", populate_by_name=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py b/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py index 58a487dc7..c299895ce 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py @@ -1,4 +1,6 @@ -SecurityRequirement = dict[str, list[str]] +from ..untrusted_string import UntrustedString + +SecurityRequirement = dict[UntrustedString, list[UntrustedString]] """ Lists the required security schemes to execute this operation. The name used for each property MUST correspond to a security scheme declared in the diff --git a/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py b/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py index df385440c..77c681c59 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py @@ -1,7 +1,6 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict, Field +from ..untrusted_string import UntrustedString from .oauth_flows import OAuthFlows @@ -19,14 +18,14 @@ class SecurityScheme(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject """ - type: str - description: Optional[str] = None - name: Optional[str] = None - security_scheme_in: Optional[str] = Field(default=None, alias="in") - scheme: Optional[str] = None - bearerFormat: Optional[str] = None - flows: Optional[OAuthFlows] = None - openIdConnectUrl: Optional[str] = None + type: UntrustedString + description: UntrustedString | None = None + name: UntrustedString | None = None + security_scheme_in: UntrustedString | None = Field(default=None, alias="in") + scheme: UntrustedString | None = None + bearerFormat: UntrustedString | None = None + flows: OAuthFlows | None = None + openIdConnectUrl: UntrustedString | None = None model_config = ConfigDict( extra="allow", populate_by_name=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/server.py b/openapi_python_client/schema/openapi_schema_pydantic/server.py index 6bc21766c..a975dcf06 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/server.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/server.py @@ -1,7 +1,6 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .server_variable import ServerVariable @@ -13,9 +12,9 @@ class Server(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject """ - url: str - description: Optional[str] = None - variables: Optional[dict[str, ServerVariable]] = None + url: UntrustedString + description: UntrustedString | None = None + variables: dict[UntrustedString, ServerVariable] | None = None model_config = ConfigDict( extra="allow", json_schema_extra={ diff --git a/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py b/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py index 8a869c40e..4517783bc 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/server_variable.py @@ -1,7 +1,7 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class ServerVariable(BaseModel): """An object representing a Server Variable for server URL template substitution. @@ -11,7 +11,7 @@ class ServerVariable(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverVariableObject """ - enum: Optional[list[str]] = None - default: str - description: Optional[str] = None + enum: list[UntrustedString] | None = None + default: UntrustedString + description: UntrustedString | None = None model_config = ConfigDict(extra="allow") diff --git a/openapi_python_client/schema/openapi_schema_pydantic/tag.py b/openapi_python_client/schema/openapi_schema_pydantic/tag.py index acb5fdc28..a433988b5 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/tag.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/tag.py @@ -1,7 +1,6 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString from .external_documentation import ExternalDocumentation @@ -15,9 +14,9 @@ class Tag(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tagObject """ - name: str - description: Optional[str] = None - externalDocs: Optional[ExternalDocumentation] = None + name: UntrustedString + description: UntrustedString | None = None + externalDocs: ExternalDocumentation | None = None model_config = ConfigDict( extra="allow", json_schema_extra={"examples": [{"name": "pet", "description": "Pets operations"}]} ) diff --git a/openapi_python_client/schema/openapi_schema_pydantic/xml.py b/openapi_python_client/schema/openapi_schema_pydantic/xml.py index 986aa44f4..53b178aa9 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/xml.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/xml.py @@ -1,7 +1,7 @@ -from typing import Optional - from pydantic import BaseModel, ConfigDict +from ..untrusted_string import UntrustedString + class XML(BaseModel): """ @@ -16,9 +16,9 @@ class XML(BaseModel): - https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xmlObject """ - name: Optional[str] = None - namespace: Optional[str] = None - prefix: Optional[str] = None + name: UntrustedString | None = None + namespace: UntrustedString | None = None + prefix: UntrustedString | None = None attribute: bool = False wrapped: bool = False model_config = ConfigDict( diff --git a/openapi_python_client/schema/parameter_location.py b/openapi_python_client/schema/parameter_location.py index 162a7cb13..61f9dae14 100644 --- a/openapi_python_client/schema/parameter_location.py +++ b/openapi_python_client/schema/parameter_location.py @@ -1,25 +1,10 @@ -# Python 3.11 has StrEnum but breaks the old `str, Enum` hack. -# Unless this gets fixed, we need to have two implementations :( -import sys +from enum import StrEnum -if sys.version_info >= (3, 11): - from enum import StrEnum - class ParameterLocation(StrEnum): - """The places Parameters can be put when calling an Endpoint""" +class ParameterLocation(StrEnum): + """The places Parameters can be put when calling an Endpoint""" - QUERY = "query" - PATH = "path" - HEADER = "header" - COOKIE = "cookie" - -else: - from enum import Enum - - class ParameterLocation(str, Enum): - """The places Parameters can be put when calling an Endpoint""" - - QUERY = "query" - PATH = "path" - HEADER = "header" - COOKIE = "cookie" + QUERY = "query" + PATH = "path" + HEADER = "header" + COOKIE = "cookie" diff --git a/openapi_python_client/schema/ref.py b/openapi_python_client/schema/ref.py new file mode 100644 index 000000000..35d228055 --- /dev/null +++ b/openapi_python_client/schema/ref.py @@ -0,0 +1,52 @@ +from typing import Any + +from pydantic import GetCoreSchemaHandler +from pydantic_core import core_schema + +from .untrusted_string import UntrustedString + + +class Ref: + """A $ref in the schema, the raw value is protected to make sure we don't accidentally place it in the generated code.""" + + def __init__(self, value: str | UntrustedString) -> None: + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + self._value = value + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: + return core_schema.no_info_after_validator_function( + cls, + core_schema.str_schema(), + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, info_arg=True, return_schema=core_schema.any_schema() + ), + ) + + @staticmethod + def _serialize(value: "Ref", info: core_schema.SerializationInfo) -> Any: + # Only unwrap in JSON mode (used for error messages); Python mode keeps the wrapper so it + # can't leak into generated code. Returning the wrapper also avoids serializer warnings. + if info.mode == "json": + return value.get_untrusted_value() + return value + + def get_untrusted_value(self) -> str: + """ + Get the raw $ref value. + + DO NOT place this in generated code. + """ + return self._value + + def endswith(self, suffix: str) -> bool: + return self._value.endswith(suffix) + + def __hash__(self) -> int: + return hash(self._value) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Ref): + return self._value == other._value + return False # pragma: no cover diff --git a/openapi_python_client/schema/untrusted_string.py b/openapi_python_client/schema/untrusted_string.py new file mode 100644 index 000000000..e54bf2858 --- /dev/null +++ b/openapi_python_client/schema/untrusted_string.py @@ -0,0 +1,56 @@ +from typing import Any, TypeVar + +from pydantic import GetCoreSchemaHandler +from pydantic_core import CoreSchema, core_schema + +T = TypeVar("T") + + +class UntrustedString: + """A raw string in the OpenAPI document which should not be directly placed in the generated code.""" + + def __init__(self, value: str) -> None: + self._value = value + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: GetCoreSchemaHandler) -> CoreSchema: + return core_schema.no_info_after_validator_function( + cls, + core_schema.str_schema(), + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, info_arg=True, return_schema=core_schema.any_schema() + ), + ) + + @staticmethod + def _serialize(value: "UntrustedString", info: core_schema.SerializationInfo) -> Any: + # Only unwrap in JSON mode (used for error messages); Python mode keeps the wrapper so it + # can't leak into generated code. Returning the wrapper also avoids serializer warnings. + if info.mode == "json": + return value.get_untrusted_value() + return value + + def get_untrusted_value(self) -> str: + """ + Return the raw string with no transformations. + + DO NOT use this for generating code directly. It can result in arbitrary code generation. + """ + return self._value + + def startswith(self, prefix: str) -> bool: + return self._value.startswith(prefix) + + def __bool__(self) -> bool: + """Whether the raw value is non-empty. Emptiness is not sensitive, so this is safe to expose.""" + return bool(self._value) + + def __hash__(self) -> int: + return hash(self._value) + + def __eq__(self, other: object) -> bool: + if isinstance(other, UntrustedString): + return self._value == other._value + if isinstance(other, str): + return self._value == other + return False diff --git a/openapi_python_client/strings.py b/openapi_python_client/strings.py new file mode 100644 index 000000000..bfbfc9e4f --- /dev/null +++ b/openapi_python_client/strings.py @@ -0,0 +1,245 @@ +from __future__ import annotations + +import builtins +import json +import re +import unicodedata +from dataclasses import dataclass +from email.message import Message +from keyword import iskeyword +from typing import Any + +from .config import Config +from .schema.untrusted_string import UntrustedString + +DELIMITERS = r"\. _-" + + +@dataclass(frozen=True, repr=False, slots=True) +class PythonCode: + """ + A snippet of Python source code generated from `UnstrustedString`s. + + It's safe to treat as code, but _may not_ be safe to place in docstrings, f-strings, etc. + """ + + _code: str + + def as_unembedded_code(self) -> str: + """ + Return the raw source code with no transformations. + + DO NOT use this inside a string context (docstrings, string literals, f-string literal text). + Pass this through the appropriate escaping function first (e.g. `safe_for_docstring`). + """ + return self._code + + +class PythonIdentifier(str): + """A snake_case string which has been validated / transformed into a valid identifier for Python""" + + def __new__(cls, value: UntrustedString | str, prefix: str, skip_snake_case: bool = False) -> PythonIdentifier: + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + safe_value = sanitize(value) + leading_underscore = safe_value.startswith("_") + if skip_snake_case: + # Keep the name as close to the original as possible (to disambiguate conflicts); + # only strip the delimiters sanitize() preserved, as they aren't valid in identifiers + safe_value = re.sub(r"[^\w]+", "", safe_value) + else: + safe_value = snake_case(safe_value) + safe_value = fix_reserved_words(safe_value) + + if not safe_value.isidentifier() or leading_underscore: + safe_value = f"{prefix}{safe_value}" + return str.__new__(cls, safe_value) + + def __deepcopy__(self, _: Any) -> PythonIdentifier: + return self + + +class ClassName(str): + """A PascalCase string which has been validated / transformed into a valid class name for Python""" + + def __new__(cls, value: UntrustedString | str, prefix: str) -> ClassName: + safe_value = fix_reserved_words(pascal_case(value)) + + if not safe_value.isidentifier(): + safe_value = f"{prefix}{safe_value}" + safe_value = fix_reserved_words(pascal_case(safe_value)) + return str.__new__(cls, safe_value) + + def __deepcopy__(self, _: Any) -> ClassName: + return self + + +def sanitize(value: str) -> str: + """Removes every character that isn't 0-9, A-Z, a-z, or a known delimiter""" + return "".join(c for c in value if c.isidentifier() or c.isdecimal() or c in DELIMITERS) + + +def split_words(value: str) -> list[str]: + """Split a string on words and known delimiters""" + if any(c.isupper() for c in value): + value = " ".join(re.split("([A-Z]?[a-z]+)", value)) + return re.findall(rf"[^{re.escape(DELIMITERS)}]+", value) + + +RESERVED_WORDS = (set(dir(builtins)) | {"self", "true", "false", "datetime"}) - { + "id", +} + + +def fix_reserved_words(value: str) -> str: + """ + Using reserved Python words as identifiers in generated code causes problems, so this function renames them. + + Args: + value: The identifier to-be that should be renamed if it's a reserved word. + + Returns: + `value` suffixed with `_` if it was a reserved word. + """ + if value in RESERVED_WORDS or iskeyword(value): + return f"{value}_" + return value + + +def snake_case(value: str | UntrustedString) -> str: + """Converts to snake_case identifier, stripping all non-alphanumeric characters""" + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + words = split_words(sanitize(value)) + return "_".join(words).lower() + + +def pascal_case(value: str | UntrustedString) -> str: + """Converts to PascalCase identifier, stripping all non-alphanumeric characters""" + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + words = split_words(sanitize(value)) + capitalized_words = (word.capitalize() if not word.isupper() else word for word in words) + return "".join(capitalized_words) + + +def kebab_case(value: str | UntrustedString) -> str: + """Converts to kebab-case identifier, stripping all non-alphanumeric characters""" + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + words = split_words(sanitize(value)) + return "-".join(words).lower() + + +#: Escaped words in docstring content are kept shorter than this so the ``wordwrap`` filter +#: never hard-splits inside an escape sequence (the narrowest wordwrap width in templates is 90). +_MAX_ESCAPED_WORD_LENGTH = 76 + + +def _split_long_word_safely(word: str) -> str: + """Insert newlines into an over-long escaped word where they can't interrupt an escape sequence. + + wordwrap hard-splits words longer than its width at arbitrary positions. A split between the + backslashes of ``\\\\`` leaves a lone backslash at the start of the next line, where it forms an + invalid escape (``\\ `` warns today and will error in a future Python) or even a ``SyntaxError`` + (``\\U`` + non-hex is a truncated escape). Splits right after an even number of backslashes are + always safe: the prefix ends with complete escapes and the suffix can't start a dangling one. + """ + chunks: list[str] = [] + start = 0 + while len(word) - start > _MAX_ESCAPED_WORD_LENGTH: + end = start + _MAX_ESCAPED_WORD_LENGTH + run = 0 + while end > start and word[end - 1] == "\\": + end -= 1 + run += 1 + # Keep an even number of trailing backslashes in this chunk. Odd runs only occur right + # before an escaped quote, so the one left behind always forms a valid `\"`. + end += run - (run % 2) + chunks.append(word[start:end]) + start = end + chunks.append(word[start:]) + return "\n".join(chunks) + + +def safe_for_docstring(value: Any) -> str: + """ + Remove any risky escapes from docstring content. + + The output is safe to run through the ``wordwrap`` filter at any width greater than + ``_MAX_ESCAPED_WORD_LENGTH``: over-long words containing escape sequences are pre-split where + no escape sequence is interrupted, so wordwrap's hard-splitting can't create invalid escapes. + """ + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + if isinstance(value, PythonCode): + value = value.as_unembedded_code() + if not isinstance(value, str): + value = str(value) + # Strip control characters which are illegal in Python source (e.g. null bytes) + value = "".join(c for c in value if unicodedata.category(c) != "Cc" or c in "\n\r\t") + + value = value.replace("\\", "\\\\").replace('"""', '\\"\\"\\"') + # Words without backslashes can be hard-split anywhere safely, so leave them alone + return re.sub( + r"\S+", + lambda match: _split_long_word_safely(match.group()) if "\\" in match.group() else match.group(), + value, + ) + + +def in_double_quote_literal(value: UntrustedString | PythonCode | str) -> str: + """Escape a string so it can be safely embedded inside a double-quoted string literal. + + The result does NOT include surrounding quotes; the caller is expected to place it inside its own `"..."` + (e.g. a Python string literal or a TOML string in generated code). JSON encoding is used for the escaping, + which is valid inside both Python and TOML double-quoted strings. + """ + if isinstance(value, UntrustedString): + value = value.get_untrusted_value() + if isinstance(value, PythonCode): + value = value.as_unembedded_code() + value = "".join( + character + for character in value + if (character in "\b\t\f" or not unicodedata.category(character).startswith("C")) + and unicodedata.category(character) not in {"Zl", "Zp"} + ) + return json.dumps(value, ensure_ascii=False)[1:-1] + + +def in_f_string_literal(value: UntrustedString | PythonCode | str) -> str: + """Escape a string so it can be safely embedded as literal text inside a double-quoted f-string. + + In addition to the double-quoted-string escaping (quotes, backslashes), this escapes `{` and `}` as + `{{` and `}}` so the value cannot be interpreted as an f-string replacement field or format specifier. + Without this, an untrusted value could inject arbitrary expressions into the generated f-string. + """ + return in_double_quote_literal(value).replace("{", "{{").replace("}", "}}") + + +def remove_string_escapes(value: str) -> str: + """Used when parsing string-literal defaults to prevent escaping the string to write arbitrary Python + + **REMOVING OR CHANGING THE USAGE OF THIS FUNCTION HAS SECURITY IMPLICATIONS** + + See Also: + - https://github.com/openapi-generators/openapi-python-client/security/advisories/GHSA-9x4c-63pf-525f + """ + return value.replace("\\", "\\\\").replace('"', r"\"") + + +def get_content_type(content_type: str, config: Config) -> str | None: + """ + Given a string representing a content type with optional parameters, returns the content type only + """ + content_type = config.content_type_overrides.get(content_type, content_type) + message = Message() + message.add_header("Content-Type", content_type) + + parsed_content_type = message.get_content_type() + if not content_type.startswith(parsed_content_type): + # Always defaults to `text/plain` if it's not recognized. We want to return an error, not default. + return None + + return parsed_content_type diff --git a/openapi_python_client/templates/README.md.jinja b/openapi_python_client/templates/README.md.jinja index ea31c83d7..526219ab6 100644 --- a/openapi_python_client/templates/README.md.jinja +++ b/openapi_python_client/templates/README.md.jinja @@ -1,5 +1,5 @@ # {{ project_name }} -{{ package_description }} +{{ package_description.get_untrusted_value() }} ## Usage First, create a client: @@ -47,7 +47,7 @@ By default, when you're calling an HTTPS API it will attempt to verify that SSL ```python client = AuthenticatedClient( - base_url="https://internal_api.example.com", + base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl="/path/to/certificate_bundle.pem", ) @@ -57,7 +57,7 @@ You can also disable certificate validation altogether, but beware that **this i ```python client = AuthenticatedClient( - base_url="https://internal_api.example.com", + base_url="https://internal_api.example.com", token="SuperSecretToken", verify_ssl=False ) @@ -76,7 +76,7 @@ Things to know: ## Advanced customizations -There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx.Client` or `httpx.AsyncClient` (depending on your use-case): +There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx2.Client` or `httpx2.AsyncClient` (depending on your use-case): ```python from {{ package_name }} import Client @@ -93,23 +93,23 @@ client = Client( httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}}, ) -# Or get the underlying httpx client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() +# Or get the underlying httpx2 client to modify directly with client.get_httpx_client() or client.get_async_httpx_client() ``` -You can even set the httpx client directly, but beware that this will override any existing settings (e.g., base_url): +You can even set the httpx2 client directly, but beware that this will override any existing settings (e.g., base_url): ```python -import httpx +import httpx2 from {{ package_name }} import Client client = Client( base_url="https://api.example.com", ) # Note that base_url needs to be re-set, as would any shared cookies, headers, etc. -client.set_httpx_client(httpx.Client(base_url="https://api.example.com", proxies="http://localhost:8030")) +client.set_httpx_client(httpx2.Client(base_url="https://api.example.com", proxy="http://localhost:8030")) ``` -{% if poetry %} +{% if meta == "poetry" %} ## Building / publishing this package This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics: 1. Update the metadata in pyproject.toml (e.g. authors, version) @@ -123,4 +123,17 @@ If you want to install this client into another project without publishing it (e 1. If that project is not using Poetry: 1. Build a wheel with `poetry build -f wheel` 1. Install that wheel from the other project `pip install ` -{% endif %} \ No newline at end of file +{% elif meta == 'uv' %} +## Building / publishing this package +This project uses [uv](https://github.com/astral-sh/uv) to manage dependencies and packaging. Here are the basics: +1. Update the metadata in `pyproject.toml` (e.g. authors, version). +2. If you're using a private repository: https://docs.astral.sh/uv/guides/integration/alternative-indexes/ +3. Build a distribution with `uv build`, builds `sdist` and `wheel` by default. +1. Publish the client with `uv publish`, see documentation for publishing to private indexes. + +If you want to install this client into another project without publishing it (e.g. for development) then: +1. If that project **is using uv**, you can simply do `uv add ` from that project +1. If that project is not using uv: + 1. Build a wheel with `uv build --wheel`. + 1. Install that wheel from the other project `pip install `. +{% endif %} diff --git a/openapi_python_client/templates/client.py.jinja b/openapi_python_client/templates/client.py.jinja index aee6096e9..13a662b1a 100644 --- a/openapi_python_client/templates/client.py.jinja +++ b/openapi_python_client/templates/client.py.jinja @@ -1,16 +1,41 @@ import ssl -from typing import Any, Union, Optional +from typing import Any, Self from attrs import define, field, evolve -import httpx - +import httpx2 + + +{% set attrs_info = { + "raise_on_unexpected_status": namespace( + type="bool", + default="field(default=False, kw_only=True)", + docstring="Whether or not to raise an errors.UnexpectedStatus if the API returns a status code" + " that was not documented in the source OpenAPI document. Can also be provided as a keyword" + " argument to the constructor." + ), + "token": namespace(type="str", default="", docstring="The token to use for authentication"), + "prefix": namespace(type="str", default='"Bearer"', docstring="The prefix to use for the Authorization header"), + "auth_header_name": namespace(type="str", default='"Authorization"', docstring="The name of the Authorization header"), +} %} + +{% macro attr_in_class_docstring(name) %} +{{ name }}: {{ attrs_info[name].docstring }} +{%- endmacro %} + +{% macro declare_attr(name) %} +{% set attr = attrs_info[name] %} +{{ name }}: {{ attr.type }}{% if attr.default %} = {{ attr.default }}{% endif %} +{% if attr.docstring and config.docstrings_on_attributes +%} +"""{{ attr.docstring }}""" +{%- endif %} +{% endmacro %} @define class Client: """A class for keeping track of data related to the API -{% macro httpx_args_docstring() %} - The following are accepted as keyword arguments and will be used to construct httpx Clients internally: +{% macro httpx2_args_docstring() %} + The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally: ``base_url``: The base URL for the API, all requests are made to a relative path to this URL @@ -19,33 +44,33 @@ class Client: ``headers``: A dictionary of headers to be sent with every request ``timeout``: The maximum amount of a time a request can take. API functions will raise - httpx.TimeoutException if this is exceeded. + httpx2.TimeoutException if this is exceeded. ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, but can be set to False for testing purposes. ``follow_redirects``: Whether or not to follow redirects. Default value is False. - ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor. {% endmacro %} -{{ httpx_args_docstring() }} +{{ httpx2_args_docstring() }} +{% if not config.docstrings_on_attributes %} Attributes: - raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a - status code that was not documented in the source OpenAPI document. Can also be provided as a keyword - argument to the constructor. + {{ attr_in_class_docstring("raise_on_unexpected_status") | wordwrap(101) | indent(12) }} +{% endif %} """ {% macro attributes() %} - raise_on_unexpected_status: bool = field(default=False, kw_only=True) + {{ declare_attr("raise_on_unexpected_status") | indent(4) }} _base_url: str = field(alias="base_url") _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") - _client: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) + _client: httpx2.Client | None = field(default=None, init=False) + _async_client: httpx2.AsyncClient | None = field(default=None, init=False) {% endmacro %}{{ attributes() }} {% macro builders(self) %} def with_headers(self, headers: dict[str, str]) -> "{{ self }}": @@ -64,30 +89,30 @@ class Client: self._async_client.cookies.update(cookies) return evolve(self, cookies={**self._cookies, **cookies}) - def with_timeout(self, timeout: httpx.Timeout) -> "{{ self }}": - """Get a new client matching this one with a new timeout (in seconds)""" + def with_timeout(self, timeout: httpx2.Timeout) -> "{{ self }}": + """Get a new client matching this one with a new timeout configuration""" if self._client is not None: self._client.timeout = timeout if self._async_client is not None: self._async_client.timeout = timeout return evolve(self, timeout=timeout) {% endmacro %}{{ builders("Client") }} -{% macro httpx_stuff(name, custom_constructor=None) %} - def set_httpx_client(self, client: httpx.Client) -> "{{ name }}": - """Manually set the underlying httpx.Client +{% macro httpx2_stuff(name, custom_constructor=None) %} + def set_httpx_client(self, client: httpx2.Client) -> Self: + """Manually set the underlying httpx2.Client **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._client = client return self - def get_httpx_client(self) -> httpx.Client: - """Get the underlying httpx.Client, constructing a new one if not previously set""" + def get_httpx_client(self) -> httpx2.Client: + """Get the underlying httpx2.Client, constructing a new one if not previously set""" if self._client is None: {% if custom_constructor %} {{ custom_constructor | indent(12) }} {% endif %} - self._client = httpx.Client( + self._client = httpx2.Client( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -98,30 +123,30 @@ class Client: ) return self._client - def __enter__(self) -> "{{ name }}": - """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + def __enter__(self) -> Self: + """Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)""" self.get_httpx_client().__enter__() return self def __exit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for internal httpx.Client (see httpx docs)""" + """Exit a context manager for internal httpx2.Client (see httpx2 docs)""" self.get_httpx_client().__exit__(*args, **kwargs) - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "{{ name }}": - """Manually the underlying httpx.AsyncClient + def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self: + """Manually set the underlying httpx2.AsyncClient **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. """ self._async_client = async_client return self - def get_async_httpx_client(self) -> httpx.AsyncClient: - """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + def get_async_httpx_client(self) -> httpx2.AsyncClient: + """Get the underlying httpx2.AsyncClient, constructing a new one if not previously set""" if self._async_client is None: {% if custom_constructor %} {{ custom_constructor | indent(12) }} {% endif %} - self._async_client = httpx.AsyncClient( + self._async_client = httpx2.AsyncClient( base_url=self._base_url, cookies=self._cookies, headers=self._headers, @@ -132,35 +157,35 @@ class Client: ) return self._async_client - async def __aenter__(self) -> "{{ name }}": - """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + async def __aenter__(self) -> Self: + """Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)""" await self.get_async_httpx_client().__aenter__() return self async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + """Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) -{% endmacro %}{{ httpx_stuff("Client") }} +{% endmacro %}{{ httpx2_stuff("Client") }} @define class AuthenticatedClient: """A Client which has been authenticated for use on secured endpoints -{{ httpx_args_docstring() }} +{{ httpx2_args_docstring() }} +{% if not config.docstrings_on_attributes %} Attributes: - raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a - status code that was not documented in the source OpenAPI document. Can also be provided as a keyword - argument to the constructor. - token: The token to use for authentication - prefix: The prefix to use for the Authorization header - auth_header_name: The name of the Authorization header + {{ attr_in_class_docstring("raise_on_unexpected_status") | wordwrap(101) | indent(12) }} + {{ attr_in_class_docstring("token") | indent(8) }} + {{ attr_in_class_docstring("prefix") | indent(8) }} + {{ attr_in_class_docstring("auth_header_name") | indent(8) }} +{% endif %} """ {{ attributes() }} - token: str - prefix: str = "Bearer" - auth_header_name: str = "Authorization" + {{ declare_attr("token") | indent(4) }} + {{ declare_attr("prefix") | indent(4) }} + {{ declare_attr("auth_header_name") | indent(4) }} {{ builders("AuthenticatedClient") }} -{{ httpx_stuff("AuthenticatedClient", "self._headers[self.auth_header_name] = f\"{self.prefix} {self.token}\" if self.prefix else self.token") }} +{{ httpx2_stuff("AuthenticatedClient", "self._headers[self.auth_header_name] = f\"{self.prefix} {self.token}\" if self.prefix else self.token") }} diff --git a/openapi_python_client/templates/endpoint_init.py.jinja b/openapi_python_client/templates/endpoint_init.py.jinja index e69de29bb..c9921b5fd 100644 --- a/openapi_python_client/templates/endpoint_init.py.jinja +++ b/openapi_python_client/templates/endpoint_init.py.jinja @@ -0,0 +1 @@ +""" Contains endpoint functions for accessing the API """ diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 2da580af4..8c35bba07 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -1,5 +1,5 @@ {% from "property_templates/helpers.jinja" import guarded_statement %} -{% from "helpers.jinja" import safe_docstring %} +{% from "helpers.jinja" import docstring as _docstring %} {% macro header_params(endpoint) %} {% if endpoint.header_parameters or endpoint.bodies | length > 0 %} @@ -12,7 +12,7 @@ headers: dict[str, Any] = {} {% else %} {% set expression = parameter.python_name %} {% endif %} - {% set statement = 'headers["' + parameter.name + '"]' + " = " + expression %} + {% set statement = 'headers["' + (parameter.name | in_double_quote_literal) + '"]' + " = " + expression %} {{ guarded_statement(parameter, parameter.python_name, statement) }} {% endfor %} {% endif %} @@ -24,10 +24,10 @@ headers: dict[str, Any] = {} cookies = {} {% for parameter in endpoint.cookie_parameters %} {% if parameter.required %} -cookies["{{ parameter.name}}"] = {{ parameter.python_name }} +cookies["{{ parameter.name | in_double_quote_literal }}"] = {{ parameter.python_name }} {% else %} if {{ parameter.python_name }} is not UNSET: - cookies["{{ parameter.name}}"] = {{ parameter.python_name }} + cookies["{{ parameter.name | in_double_quote_literal }}"] = {{ parameter.python_name }} {% endif %} {% endfor %} @@ -47,7 +47,7 @@ params: dict[str, Any] = {} {{ prop_template.transform(property, property.python_name, destination) }} {% endif %} {%- if not property.json_is_dict %} -params["{{ property.name }}"] = {{ destination }} +params["{{ property.name | in_double_quote_literal }}"] = {{ destination }} {% else %} {{ guarded_statement(property, destination, "params.update(" + destination + ")") }} {% endif %} @@ -58,33 +58,46 @@ params = {k: v for k, v in params.items() if v is not UNSET and v is not None} {% endif %} {% endmacro %} -{% macro body_to_kwarg(body, destination) %} +{% macro body_to_kwarg(body) %} {% if body.body_type == "data" %} -{{ destination }} = body.to_dict() + {% if body.prop.required %} +_kwargs["data"] = body.to_dict() + {% else %} +if not isinstance(body, Unset): + _kwargs["data"] = body.to_dict() + {% endif %} {% elif body.body_type == "files"%} -{{ multipart_body(body, destination) }} +{{ multipart_body(body) }} {% elif body.body_type == "json" %} -{{ json_body(body, destination) }} +{{ json_body(body) }} {% elif body.body_type == "content" %} -{{ destination }} = body.payload + {% if body.prop.required %} +_kwargs["content"] = body.payload + {% else %} +if not isinstance(body, Unset): + _kwargs["content"] = body.payload + {% endif %} {% endif %} {% endmacro %} -{% macro json_body(body, destination) %} +{% macro json_body(body) %} {% set property = body.prop %} {% import "property_templates/" + property.template as prop_template %} {% if prop_template.transform %} -{{ prop_template.transform(property, property.python_name, destination) }} +{{ prop_template.transform(property, property.python_name, "_kwargs[\"json\"]", skip_unset=True, declare_type=False) }} +{% elif property.required %} +_kwargs["json"] = {{ property.python_name }} {% else %} -{{ destination }} = {{ property.python_name }} +if not isinstance({{property.python_name}}, Unset): + _kwargs["json"] = {{ property.python_name }} {% endif %} {% endmacro %} -{% macro multipart_body(body, destination) %} +{% macro multipart_body(body) %} {% set property = body.prop %} {% import "property_templates/" + property.template as prop_template %} {% if prop_template.transform_multipart_body %} -{{ prop_template.transform_multipart_body(property, property.python_name, destination) }} +{{ prop_template.transform_multipart_body(property) }} {% endif %} {% endmacro %} @@ -92,7 +105,7 @@ params = {k: v for k, v in params.items() if v is not UNSET and v is not None} {% macro arguments(endpoint, include_client=True) %} {# path parameters #} {% for parameter in endpoint.path_parameters %} -{{ parameter.to_string() }}, +{{ parameter.to_string() | as_unembedded_code }}, {% endfor %} {% if include_client or ((endpoint.list_all_parameters() | length) > (endpoint.path_parameters | length)) %} *, @@ -102,29 +115,29 @@ params = {k: v for k, v in params.items() if v is not UNSET and v is not None} {% if endpoint.requires_security %} client: AuthenticatedClient, {% else %} -client: Union[AuthenticatedClient, Client], +client: AuthenticatedClient | Client, {% endif %} {% endif %} {# Any allowed bodies #} {% if endpoint.bodies | length == 1 %} -body: {{ endpoint.bodies[0].prop.get_type_string() }}, +body: {{ endpoint.bodies[0].prop.get_type_string() | as_unembedded_code }}{% if not endpoint.bodies[0].prop.required %} = UNSET{% endif %}, {% elif endpoint.bodies | length > 1 %} -body: Union[ - {% for body in endpoint.bodies %} - {{ body.prop.get_type_string() }}, - {% endfor %} -], +body: + {%- for body in endpoint.bodies -%}{% set body_required = body_required and body.prop.required %} + {{ body.prop.get_type_string(no_optional=True) | as_unembedded_code }} {% if not loop.last %} | {% endif %} + {%- endfor -%}{% if not body_required %} | Unset = UNSET{% endif %} +, {% endif %} {# query parameters #} {% for parameter in endpoint.query_parameters %} -{{ parameter.to_string() }}, +{{ parameter.to_string() | as_unembedded_code }}, {% endfor %} {% for parameter in endpoint.header_parameters %} -{{ parameter.to_string() }}, +{{ parameter.to_string() | as_unembedded_code }}, {% endfor %} {# cookie parameters #} {% for parameter in endpoint.cookie_parameters %} -{{ parameter.to_string() }}, +{{ parameter.to_string() | as_unembedded_code }}, {% endfor %} {% endmacro %} @@ -151,10 +164,10 @@ body=body, {% endmacro %} {% macro docstring_content(endpoint, return_string, is_detailed) %} -{% if endpoint.summary %}{{ endpoint.summary | wordwrap(100)}} +{% if endpoint.summary %}{{ endpoint.summary | safe_for_docstring | wordwrap(100)}} {% endif -%} -{%- if endpoint.description %} {{ endpoint.description | wordwrap(100) }} +{%- if endpoint.description %} {{ endpoint.description | safe_for_docstring | wordwrap(100) }} {% endif %} {% if not endpoint.summary and not endpoint.description %} @@ -171,16 +184,31 @@ Args: {% endif %} Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. - httpx.TimeoutException: If the request takes longer than Client.timeout. + httpx2.TimeoutException: If the request takes longer than Client.timeout. Returns: {% if is_detailed %} - Response[{{ return_string }}] + Response[{{ return_string | safe_for_docstring }}] {% else %} - {{ return_string }} + {{ return_string | safe_for_docstring }} {% endif %} {% endmacro %} {% macro docstring(endpoint, return_string, is_detailed) %} -{{ safe_docstring(docstring_content(endpoint, return_string, is_detailed)) }} +{{ _docstring(docstring_content(endpoint, return_string, is_detailed)) }} +{% endmacro %} + +{% macro parse_response(parsed_responses, response) %} +{% if parsed_responses %}{% import "property_templates/" + response.prop.template as prop_template %} +{% if prop_template.construct %} +{{ prop_template.construct(response.prop, response.source.attribute) }} +{% elif response.source.return_type == response.prop.get_type_string() | as_unembedded_code %} +{{ response.prop.python_name }} = {{ response.source.attribute }} +{% else %} +{{ response.prop.python_name }} = cast({{ response.prop.get_type_string() | as_unembedded_code }}, {{ response.source.attribute }}) +{% endif %} +return {{ response.prop.python_name }} +{% else %} +return None +{% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 35090614c..48caf194b 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -1,7 +1,8 @@ from http import HTTPStatus -from typing import Any, Optional, Union, cast +from typing import Any, cast +from urllib.parse import quote -import httpx +import httpx2 from ...client import AuthenticatedClient, Client from ...types import Response, UNSET @@ -15,7 +16,7 @@ from ... import errors arguments, client, kwargs, parse_response, docstring, body_to_kwarg %} {% set return_string = endpoint.response_type() %} -{% set parsed_responses = (endpoint.responses | length > 0) and return_string != "Any" %} +{% set parsed_responses = (endpoint.responses | length > 0) and return_string != PythonCode("Any") %} def _get_kwargs( {{ arguments(endpoint, include_client=False) | indent(4) }} @@ -29,13 +30,13 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "{{ endpoint.method }}", {% if endpoint.path_parameters %} - "url": "{{ endpoint.path }}".format( + "url": "{{ endpoint.path | in_double_quote_literal }}".format( {%- for parameter in endpoint.path_parameters -%} - {{parameter.python_name}}={{parameter.python_name}}, + {{parameter.python_name}}=quote(str({{parameter.python_name}}), safe=""), {%- endfor -%} ), {% else %} - "url": "{{ endpoint.path }}", + "url": "{{ endpoint.path | in_double_quote_literal }}", {% endif %} {% if endpoint.query_parameters %} "params": params, @@ -47,18 +48,18 @@ def _get_kwargs( {% if endpoint.bodies | length > 1 %} {% for body in endpoint.bodies %} - if isinstance(body, {{body.prop.get_type_string() }}): - {% set destination = "_" + body.body_type + "_body" %} - {{ body_to_kwarg(body, destination) | indent(8) }} - _kwargs["{{ body.body_type.value }}"] = {{ destination }} - headers["Content-Type"] = "{{ body.content_type }}" + if isinstance(body, {{body.prop.get_type_string(no_optional=True) | as_unembedded_code }}): + {{ body_to_kwarg(body) | indent(8) }} + {#- httpx2 sets multipart/form-data with a random boundary itself, a fixed one could appear in the content #} + {%- if body.content_type != "multipart/form-data" %} + headers["Content-Type"] = "{{ body.content_type | in_double_quote_literal }}" + {% endif %} {% endfor %} {% elif endpoint.bodies | length == 1 %} {% set body = endpoint.bodies[0] %} - {{ body_to_kwarg(body, "_body") | indent(4) }} - _kwargs["{{ body.body_type.value }}"] = _body - {% if body.content_type != "multipart/form-data" %}{# Need httpx to set the boundary automatically #} - headers["Content-Type"] = "{{ body.content_type }}" + {{ body_to_kwarg(body) | indent(4) }} + {%- if body.content_type != "multipart/form-data" %} + headers["Content-Type"] = "{{ body.content_type | in_double_quote_literal }}" {% endif %} {% endif %} @@ -67,30 +68,34 @@ def _get_kwargs( {% endif %} return _kwargs +{% if endpoint.responses.default %} + {% set return_type = return_string | as_unembedded_code %} +{% else %} + {% set return_type = (return_string | as_unembedded_code) + " | None" %} +{% endif %} -def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[{{ return_string }}]: - {% for response in endpoint.responses %} - if response.status_code == {{ response.status_code.value }}: - {% if parsed_responses %}{% import "property_templates/" + response.prop.template as prop_template %} - {% if prop_template.construct %} - {{ prop_template.construct(response.prop, response.source.attribute) | indent(8) }} - {% elif response.source.return_type == response.prop.get_type_string() %} - {{ response.prop.python_name }} = {{ response.source.attribute }} - {% else %} - {{ response.prop.python_name }} = cast({{ response.prop.get_type_string() }}, {{ response.source.attribute }}) - {% endif %} - return {{ response.prop.python_name }} - {% else %} - return None - {% endif %} + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> {{return_type}}: + {% for response in endpoint.responses.patterns %} + {% set code_range = response.status_code.range %} + {% if code_range[0] == code_range[1] %} + if response.status_code == {{ code_range[0] }}: + {% else %} + if {{ code_range[0] }} <= response.status_code <= {{ code_range[1] }}: + {% endif %} + {{ parse_response(parsed_responses, response) | indent(8) }} {% endfor %} + {% if endpoint.responses.default %} + {{ parse_response(parsed_responses, endpoint.responses.default) | indent(4) }} + {% else %} if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) else: return None + {% endif %} -def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[{{ return_string }}]: +def _build_response(*, client: AuthenticatedClient | Client, response: httpx2.Response) -> Response[{{ return_string | as_unembedded_code }}]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -101,7 +106,7 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt def sync_detailed( {{ arguments(endpoint) | indent(4) }} -) -> Response[{{ return_string }}]: +) -> Response[{{ return_string | as_unembedded_code }}]: {{ docstring(endpoint, return_string, is_detailed=true) | indent(4) }} kwargs = _get_kwargs( @@ -117,7 +122,7 @@ def sync_detailed( {% if parsed_responses %} def sync( {{ arguments(endpoint) | indent(4) }} -) -> Optional[{{ return_string }}]: +) -> {{ return_string | as_unembedded_code }} | None: {{ docstring(endpoint, return_string, is_detailed=false) | indent(4) }} return sync_detailed( @@ -127,7 +132,7 @@ def sync( async def asyncio_detailed( {{ arguments(endpoint) | indent(4) }} -) -> Response[{{ return_string }}]: +) -> Response[{{ return_string | as_unembedded_code }}]: {{ docstring(endpoint, return_string, is_detailed=true) | indent(4) }} kwargs = _get_kwargs( @@ -143,7 +148,7 @@ async def asyncio_detailed( {% if parsed_responses %} async def asyncio( {{ arguments(endpoint) | indent(4) }} -) -> Optional[{{ return_string }}]: +) -> {{ return_string | as_unembedded_code }} | None: {{ docstring(endpoint, return_string, is_detailed=false) | indent(4) }} return (await asyncio_detailed( diff --git a/openapi_python_client/templates/helpers.jinja b/openapi_python_client/templates/helpers.jinja index 180613c02..4302dbc09 100644 --- a/openapi_python_client/templates/helpers.jinja +++ b/openapi_python_client/templates/helpers.jinja @@ -1,8 +1,6 @@ -{% macro safe_docstring(content) %} -{# This macro returns the provided content as a docstring, set to a raw string if it contains a backslash #} -{% if '\\' in content -%} -r""" {{ content }} """ -{%- else -%} +{% macro docstring(content) %} +{# This macro returns the provided content as a docstring unless it's empty, then it emits nothing. #} +{% if (content | trim) %} """ {{ content }} """ {%- endif -%} {% endmacro %} \ No newline at end of file diff --git a/openapi_python_client/templates/literal_enum.py.jinja b/openapi_python_client/templates/literal_enum.py.jinja index 72207efa3..852c533bc 100644 --- a/openapi_python_client/templates/literal_enum.py.jinja +++ b/openapi_python_client/templates/literal_enum.py.jinja @@ -1,10 +1,10 @@ -from typing import Literal, cast +from typing import Literal {{ enum.class_info.name }} = Literal{{ "%r" | format(enum.values|list|sort) }} {{ enum.get_class_name_snake_case() | upper }}_VALUES: set[{{ enum.class_info.name }}] = { {% for v in enum.values|list|sort %}{{"%r"|format(v)}}, {% endfor %} } -def check_{{ enum.get_class_name_snake_case() }}(value: {{ enum.get_instance_type_string() }}) -> {{ enum.class_info.name}}: +def check_{{ enum.get_class_name_snake_case() }}(value: {{ enum.get_instance_type_string() | as_unembedded_code }}) -> {{ enum.class_info.name}}: if value in {{ enum.get_class_name_snake_case() | upper }}_VALUES: - return cast({{enum.class_info.name}}, value) + return value raise TypeError(f"Unexpected value {value!r}. Expected one of {{"{"}}{{ enum.get_class_name_snake_case() | upper }}_VALUES!r}") diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 739f68962..141635127 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -1,9 +1,13 @@ -from typing import Any, TypeVar, Optional, BinaryIO, TextIO, TYPE_CHECKING +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator from attrs import define as _attrs_define from attrs import field as _attrs_field {% if model.is_multipart_body %} import json +from .. import types {% endif %} from ..types import UNSET, Unset @@ -12,7 +16,7 @@ from ..types import UNSET, Unset {{ relative }} {% endfor %} -{% for lazy_import in model.lazy_imports %} +{% for lazy_import in model.lazy_imports | sort %} {% if loop.first %} if TYPE_CHECKING: {% endif %} @@ -21,21 +25,21 @@ if TYPE_CHECKING: {% if model.additional_properties %} -{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string(quoted=not model.additional_properties.is_base_type) %} +{% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string() | as_unembedded_code %} {% endif %} {% set class_name = model.class_info.name %} {% set module_name = model.class_info.module_name %} -{% from "helpers.jinja" import safe_docstring %} +{% from "helpers.jinja" import docstring %} T = TypeVar("T", bound="{{ class_name }}") {% macro class_docstring_content(model) %} - {% if model.title %}{{ model.title | wordwrap(116) }} + {% if model.title %}{{ model.title | safe_for_docstring | wordwrap(116) }} {% endif -%} - {%- if model.description %}{{ model.description | wordwrap(116) }} + {%- if model.description %}{{ model.description | safe_for_docstring | wordwrap(116) }} {% endif %} {% if not model.title and not model.description %} @@ -44,65 +48,87 @@ T = TypeVar("T", bound="{{ class_name }}") {% endif %} {% if model.example %} Example: - {{ model.example | string | wordwrap(112) | indent(12) }} + {{ model.example | safe_for_docstring | wordwrap(112) | indent(12) }} {% endif %} - {% if model.required_properties or model.optional_properties %} + {% if (not config.docstrings_on_attributes) and (model.required_properties or model.optional_properties) %} Attributes: {% for property in model.required_properties + model.optional_properties %} {{ property.to_docstring() | wordwrap(112) | indent(12) }} {% endfor %}{% endif %} {% endmacro %} +{% macro declare_property(property) %} +{%- if config.docstrings_on_attributes and property.description -%} +{{ property.to_string() | as_unembedded_code }} +{{ docstring(property.description | safe_for_docstring) | wordwrap(112) }} +{%- else -%} +{{ property.to_string() | as_unembedded_code }} +{%- endif -%} +{% endmacro %} + @_attrs_define class {{ class_name }}: - {{ safe_docstring(class_docstring_content(model)) | indent(4) }} + {{ docstring(class_docstring_content(model)) | indent(4) }} {% for property in model.required_properties + model.optional_properties %} {% if property.default is none and property.required %} - {{ property.to_string() }} + {{ declare_property(property) | indent(4) }} {% endif %} {% endfor %} {% for property in model.required_properties + model.optional_properties %} {% if property.default is not none or not property.required %} - {{ property.to_string() }} + {{ declare_property(property) | indent(4) }} {% endif %} {% endfor %} {% if model.additional_properties %} additional_properties: dict[str, {{ additional_property_type }}] = _attrs_field(init=False, factory=dict) {% endif %} -{% macro _to_dict(multipart=False) %} -{% for property in model.required_properties + model.optional_properties %} +{% macro _transform_property(property, content) %} +{% import "property_templates/" + property.template as prop_template %} +{%- if prop_template.transform -%} +{{ prop_template.transform(property=property, source=content, destination=property.python_name) }} +{%- else -%} +{{ property.python_name }} = {{ content }} +{%- endif -%} +{% endmacro %} + +{% macro multipart(property, source, destination) %} {% import "property_templates/" + property.template as prop_template %} -{% if multipart %} -{{ prop_template.transform_multipart(property, "self." + property.python_name, property.python_name) }} -{% elif prop_template.transform %} -{{ prop_template.transform(property=property, source="self." + property.python_name, destination=property.python_name) }} +{% if not property.required %} +if not isinstance({{source}}, Unset): + {{ prop_template.multipart(property, source, destination) | indent(4) }} {% else %} -{{ property.python_name }} = self.{{ property.python_name }} +{{ prop_template.multipart(property, source, destination) }} {% endif %} +{% endmacro %} -{% endfor %} - +{% macro _prepare_field_dict() %} field_dict: dict[str, Any] = {} {% if model.additional_properties %} {% import "property_templates/" + model.additional_properties.template as prop_template %} -{% if multipart %} -for prop_name, prop in self.additional_properties.items(): - {{ prop_template.transform_multipart(model.additional_properties, "prop", "field_dict[prop_name]") | indent(4) }} -{% elif prop_template.transform %} +{% if prop_template.transform %} for prop_name, prop in self.additional_properties.items(): {{ prop_template.transform(model.additional_properties, "prop", "field_dict[prop_name]", declare_type=false) | indent(4) }} {% else %} field_dict.update(self.additional_properties) -{% endif %} -{% endif %} +{%- endif -%} +{%- endif -%} +{% endmacro %} + +{% macro _to_dict() %} +{% for property in model.required_properties + model.optional_properties -%} +{{ _transform_property(property, "self." + property.python_name) }} + +{% endfor %} + +{{ _prepare_field_dict() }} {% if model.required_properties | length > 0 or model.optional_properties | length > 0 %} field_dict.update({ {% for property in model.required_properties + model.optional_properties %} {% if property.required %} - "{{ property.name }}": {{ property.python_name }}, + "{{ property.name | in_double_quote_literal }}": {{ property.python_name }}, {% endif %} {% endfor %} }) @@ -110,7 +136,7 @@ field_dict.update({ {% for property in model.optional_properties %} {% if not property.required %} if {{ property.python_name }} is not UNSET: - field_dict["{{ property.name }}"] = {{ property.python_name }} + field_dict["{{ property.name | in_double_quote_literal }}"] = {{ property.python_name }} {% endif %} {% endfor %} @@ -118,28 +144,45 @@ return field_dict {% endmacro %} def to_dict(self) -> dict[str, Any]: - {% for lazy_import in model.lazy_imports %} - {{ lazy_import }} + {% for lazy_import in model.lazy_imports | sort %} + {{ lazy_import }} # noqa: PLC0415 {% endfor %} {{ _to_dict() | indent(8) }} {% if model.is_multipart_body %} - def to_multipart(self) -> dict[str, Any]: - {{ _to_dict(multipart=True) | indent(8) }} + def to_multipart(self) -> types.RequestFiles: + {% for lazy_import in model.lazy_imports | sort %} + {{ lazy_import }} # noqa: PLC0415 + {% endfor %} + files: types.RequestFiles = [] + + {% for property in model.required_properties + model.optional_properties %} + {% set destination = "\"" + (property.name | in_double_quote_literal) + "\"" %} + {{ multipart(property, "self." + property.python_name, destination) | indent(8) }} + + {% endfor %} + + {% if model.additional_properties %} + for prop_name, prop in self.additional_properties.items(): + {{ multipart(model.additional_properties, "prop", "prop_name") | indent(4) }} + {% endif %} + + return files + {% endif %} @classmethod - def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T: - {% for lazy_import in model.lazy_imports %} - {{ lazy_import }} + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + {% for lazy_import in model.lazy_imports | sort %} + {{ lazy_import }} # noqa: PLC0415 {% endfor %} {% if (model.required_properties or model.optional_properties or model.additional_properties) %} - d = src_dict.copy() + d = dict(src_dict) {% for property in model.required_properties + model.optional_properties %} {% if property.required %} - {% set property_source = 'd.pop("' + property.name + '")' %} + {% set property_source = 'd.pop("' + (property.name | in_double_quote_literal) + '")' %} {% else %} - {% set property_source = 'd.pop("' + property.name + '", UNSET)' %} + {% set property_source = 'd.pop("' + (property.name | in_double_quote_literal) + '", UNSET)' %} {% endif %} {% import "property_templates/" + property.template as prop_template %} {% if prop_template.construct %} @@ -161,8 +204,8 @@ return field_dict {% import "property_templates/" + model.additional_properties.template as prop_template %} {% if model.additional_properties.lazy_imports %} - {% for lazy_import in model.additional_properties.lazy_imports %} - {{ lazy_import }} + {% for lazy_import in model.additional_properties.lazy_imports | sort %} + {{ lazy_import }} # noqa: PLC0415 {% endfor %} {% endif %} {% else %} diff --git a/openapi_python_client/templates/package_init.py.jinja b/openapi_python_client/templates/package_init.py.jinja index ecf60e74d..2854eb6f1 100644 --- a/openapi_python_client/templates/package_init.py.jinja +++ b/openapi_python_client/templates/package_init.py.jinja @@ -1,6 +1,6 @@ -{% from "helpers.jinja" import safe_docstring %} +{% from "helpers.jinja" import docstring %} -{{ safe_docstring(package_description) }} +{{ docstring(package_description | safe_for_docstring) }} from .client import AuthenticatedClient, Client __all__ = ( diff --git a/openapi_python_client/templates/property_templates/any_property.py.jinja b/openapi_python_client/templates/property_templates/any_property.py.jinja index 25a16516a..ad3f195a4 100644 --- a/openapi_python_client/templates/property_templates/any_property.py.jinja +++ b/openapi_python_client/templates/property_templates/any_property.py.jinja @@ -1,7 +1,3 @@ -{% macro transform_multipart(property, source, destination) %} -{% if not property.required %} -{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") -{% else %} -{{ destination }} = (None, str({{ source }}).encode(), "text/plain") -{% endif %} +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} \ No newline at end of file diff --git a/openapi_python_client/templates/property_templates/boolean_property.py.jinja b/openapi_python_client/templates/property_templates/boolean_property.py.jinja index 52fda08e1..e2c3392a1 100644 --- a/openapi_python_client/templates/property_templates/boolean_property.py.jinja +++ b/openapi_python_client/templates/property_templates/boolean_property.py.jinja @@ -2,10 +2,6 @@ "true" if {{ source }} else "false" {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% if not property.required %} -{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") -{% else %} -{{ destination }} = (None, str({{ source }}).encode(), "text/plain") -{% endif %} +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/const_property.py.jinja b/openapi_python_client/templates/property_templates/const_property.py.jinja index fa635b5b9..8be52e752 100644 --- a/openapi_python_client/templates/property_templates/const_property.py.jinja +++ b/openapi_python_client/templates/property_templates/const_property.py.jinja @@ -1,5 +1,9 @@ {% macro construct(property, source) %} -{{ property.python_name }} = cast({{ property.get_type_string() }} , {{ source }}) -if {{ property.python_name }} != {{ property.value.python_code }}{% if not property.required %}and not isinstance({{ property.python_name }}, Unset){% endif %}: - raise ValueError(f"{{ property.name }} must match const {{ property.value.python_code }}, got '{{'{' + property.python_name + '}' }}'") +{{ property.python_name }} = cast({{ property.get_type_string() | as_unembedded_code }} , {{ source }}) +if {{ property.python_name }} != {{ property.value.python_code | as_unembedded_code }}{% if not property.required %}and not isinstance({{ property.python_name }}, Unset){% endif %}: + raise ValueError(f"{{ property.name | in_f_string_literal }} must match const {{ property.value.python_code | in_f_string_literal }}, got '{{'{' + property.python_name + '}' }}'") {%- endmacro %} + +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, {{ source }}, "text/plain"))) +{% endmacro %} diff --git a/openapi_python_client/templates/property_templates/date_property.py.jinja b/openapi_python_client/templates/property_templates/date_property.py.jinja index b5da665f7..8bf8d53fa 100644 --- a/openapi_python_client/templates/property_templates/date_property.py.jinja +++ b/openapi_python_client/templates/property_templates/date_property.py.jinja @@ -1,5 +1,5 @@ {% macro construct_function(property, source) %} -isoparse({{ source }}).date() +datetime.date.fromisoformat({{ source }}) {% endmacro %} {% from "property_templates/property_macros.py.jinja" import construct_template %} @@ -10,30 +10,24 @@ isoparse({{ source }}).date() {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% set transformed = source + ".isoformat()" %} {% if property.required %} {{ destination }} = {{ transformed }} {%- else %} -{% if declare_type %} -{% set type_annotation = property.get_type_string(json=True) %} +{% if not skip_unset %} + {% if declare_type %} + {% set type_annotation = property.get_type_string(json=True) | as_unembedded_code %} {{ destination }}: {{ type_annotation }} = UNSET -{% else %} + {% else %} {{ destination }} = UNSET + {% endif %} {% endif %} if not isinstance({{ source }}, Unset): {{ destination }} = {{ transformed }} {%- endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% set transformed = source + ".isoformat().encode()" %} -{% if property.required %} -{{ destination }} = {{ transformed }} -{%- else %} -{% set type_annotation = property.get_type_string(json=True) | replace("str", "bytes") %} -{{ destination }}: {{ type_annotation }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} -{%- endif %} +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, {{ source }}.isoformat().encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/datetime_property.py.jinja b/openapi_python_client/templates/property_templates/datetime_property.py.jinja index 32e3f2ee6..f8a35e677 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.py.jinja +++ b/openapi_python_client/templates/property_templates/datetime_property.py.jinja @@ -1,5 +1,5 @@ {% macro construct_function(property, source) %} -isoparse({{ source }}) +datetime.datetime.fromisoformat({{ source }}) {% endmacro %} {% from "property_templates/property_macros.py.jinja" import construct_template %} @@ -10,30 +10,24 @@ isoparse({{ source }}) {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% set transformed = source + ".isoformat()" %} {% if property.required %} {{ destination }} = {{ transformed }} {%- else %} -{% if declare_type %} -{% set type_annotation = property.get_type_string(json=True) %} + {% if not skip_unset %} + {% if declare_type %} + {% set type_annotation = property.get_type_string(json=True) | as_unembedded_code %} {{ destination }}: {{ type_annotation }} = UNSET -{% else %} + {% else %} {{ destination }} = UNSET -{% endif %} + {% endif %} + {% endif %} if not isinstance({{ source }}, Unset): {{ destination }} = {{ transformed }} {%- endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% set transformed = source + ".isoformat().encode()" %} -{% if property.required %} -{{ destination }} = {{ transformed }} -{%- else %} -{% set type_annotation = property.get_type_string(json=True) | replace("str", "bytes") %} -{{ destination }}: {{ type_annotation }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} -{%- endif %} +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, {{ source }}.isoformat().encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/enum_property.py.jinja b/openapi_python_client/templates/property_templates/enum_property.py.jinja index c46538eac..4943ef992 100644 --- a/openapi_python_client/templates/property_templates/enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/enum_property.py.jinja @@ -10,28 +10,20 @@ {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.value_type.__name__ }}){% endmacro %} -{% macro transform(property, source, destination, declare_type=True, multipart=False) %} +{% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% set transformed = source + ".value" %} -{% set type_string = property.get_type_string(json=True) %} +{% set type_string = property.get_type_string(json=True) | as_unembedded_code %} {% if property.required %} {{ destination }} = {{ transformed }} {%- else %} -{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET +{% if not skip_unset %}{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET{% endif +%} if not isinstance({{ source }}, Unset): {{ destination }} = {{ transformed }} {% endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% set transformed = "(None, str(" + source + ".value" + ").encode(), \"text/plain\")" %} -{% set type_string = "Union[Unset, tuple[None, bytes, str]]" %} -{% if property.required %} -{{ destination }} = {{ transformed }} -{%- else %} -{{ destination }}: {{ type_string }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} -{% endif %} +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{ source }}.value).encode(), "text/plain"))) {% endmacro %} {% macro transform_header(source) %} diff --git a/openapi_python_client/templates/property_templates/file_property.py.jinja b/openapi_python_client/templates/property_templates/file_property.py.jinja index 8d27ae617..df531af81 100644 --- a/openapi_python_client/templates/property_templates/file_property.py.jinja +++ b/openapi_python_client/templates/property_templates/file_property.py.jinja @@ -12,22 +12,16 @@ File( {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, bytes){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% if property.required %} {{ destination }} = {{ source }}.to_tuple() {% else %} -{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET +{% if not skip_unset %}{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) | as_unembedded_code }}{% endif %} = UNSET{% endif +%} if not isinstance({{ source }}, Unset): {{ destination }} = {{ source }}.to_tuple() {% endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% if property.required %} -{{ destination }} = {{ source }}.to_tuple() -{% else %} -{{ destination }}: {{ property.get_type_string(json=True) }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ source }}.to_tuple() -{% endif %} +{% macro multipart(property, source, name) %} +files.append(({{ name }}, {{ source }}.to_tuple())) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/float_property.py.jinja b/openapi_python_client/templates/property_templates/float_property.py.jinja index 12ffb0fb4..dc982cb68 100644 --- a/openapi_python_client/templates/property_templates/float_property.py.jinja +++ b/openapi_python_client/templates/property_templates/float_property.py.jinja @@ -2,10 +2,6 @@ str({{ source }}) {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% if not property.required %} -{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") -{% else %} -{{ destination }} = (None, str({{ source }}).encode(), "text/plain") -{% endif %} +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/int_property.py.jinja b/openapi_python_client/templates/property_templates/int_property.py.jinja index 12ffb0fb4..dc982cb68 100644 --- a/openapi_python_client/templates/property_templates/int_property.py.jinja +++ b/openapi_python_client/templates/property_templates/int_property.py.jinja @@ -2,10 +2,6 @@ str({{ source }}) {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% if not property.required %} -{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") -{% else %} -{{ destination }} = (None, str({{ source }}).encode(), "text/plain") -{% endif %} +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{source}}).encode(), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/list_property.py.jinja b/openapi_python_client/templates/property_templates/list_property.py.jinja index 94a9c6d65..0665412fb 100644 --- a/openapi_python_client/templates/property_templates/list_property.py.jinja +++ b/openapi_python_client/templates/property_templates/list_property.py.jinja @@ -3,26 +3,28 @@ {% import "property_templates/" + inner_property.template as inner_template %} {% if inner_template.construct %} {% set inner_source = inner_property.python_name + "_data" %} +{% if property.required %} {{ property.python_name }} = [] _{{ property.python_name }} = {{ source }} -{% if property.required %} for {{ inner_source }} in (_{{ property.python_name }}): -{% else %} -for {{ inner_source }} in (_{{ property.python_name }} or []): -{% endif %} {{ inner_template.construct(inner_property, inner_source) | indent(4) }} {{ property.python_name }}.append({{ inner_property.python_name }}) {% else %} -{{ property.python_name }} = cast({{ property.get_type_string(no_optional=True) }}, {{ source }}) +_{{ property.python_name }} = {{ source }} +{{ property.python_name }}: {{ property.get_type_string() | as_unembedded_code }} = UNSET +if _{{ property.python_name }} is not UNSET: + {{ property.python_name }} = [] + for {{ inner_source }} in _{{ property.python_name }}: + {{ inner_template.construct(inner_property, inner_source) | indent(8) }} + {{ property.python_name }}.append({{ inner_property.python_name }}) +{% endif %} +{% else %} +{{ property.python_name }} = cast({{ property.get_type_string(no_optional=True) | as_unembedded_code }}, {{ source }}) {% endif %} {% endmacro %} -{% macro _transform(property, source, destination, multipart, transform_method) %} +{% macro _transform(property, source, destination, transform_method) %} {% set inner_property = property.inner_property %} -{% if multipart %} -{% set multipart_destination = destination %} -{% set destination = "_temp_" + destination %} -{% endif %} {% import "property_templates/" + inner_property.template as inner_template %} {% if inner_template.transform %} {% set inner_source = inner_property.python_name + "_data" %} @@ -33,45 +35,26 @@ for {{ inner_source }} in {{ source }}: {% else %} {{ destination }} = {{ source }} {% endif %} -{% if multipart %} -{{ multipart_destination }} = (None, json.dumps({{ destination }}).encode(), 'application/json') -{% endif %} {% endmacro %} {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, list){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} -{% set inner_property = property.inner_property %} -{% set type_string = property.get_type_string(json=True) %} -{% if property.required %} -{{ _transform(property, source, destination, False, "to_dict") }} -{% else %} -{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET -if not isinstance({{ source }}, Unset): - {{ _transform(property, source, destination, False, "to_dict") | indent(4)}} -{% endif %} -{% endmacro %} - -{% macro transform_multipart(property, source, destination) %} +{% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% set inner_property = property.inner_property %} -{% set type_string = "Union[Unset, tuple[None, bytes, str]]" %} +{% set type_string = property.get_type_string(json=True) | as_unembedded_code %} {% if property.required %} -{{ _transform(property, source, destination, True, "to_dict") }} +{{ _transform(property, source, destination, "to_dict") }} {% else %} -{{ destination }}: {{ type_string }} = UNSET +{% if not skip_unset %}{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET{% endif +%} if not isinstance({{ source }}, Unset): - {{ _transform(property, source, destination, True, "to_dict") | indent(4)}} + {{ _transform(property, source, destination, "to_dict") | indent(4)}} {% endif %} {% endmacro %} -{% macro transform_multipart_body(property, source, destination) %} +{% macro multipart(property, source, destination) %} {% set inner_property = property.inner_property %} -{% set type_string = property.get_type_string(json=True) %} -{% if property.required %} -{{ _transform(property, source, destination, False, "to_multipart") }} -{% else %} -{{ destination }}: {{ type_string }} = UNSET -if not isinstance({{ source }}, Unset): - {{ _transform(property, source, destination, False, "to_multipart") | indent(4)}} -{% endif %} +{% import "property_templates/" + inner_property.template as inner_template %} +{% set inner_source = inner_property.python_name + "_element" %} +for {{ inner_source }} in {{ source }}: + {{ inner_template.multipart(inner_property, inner_source, destination) | indent(4) }} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja b/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja index 1506284d7..b718a409b 100644 --- a/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/literal_enum_property.py.jinja @@ -8,29 +8,21 @@ check_{{ property.get_class_name_snake_case() }}({{ source }}) {{ construct_template(construct_function, property, source) }} {% endmacro %} -{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.get_instance_type_string() }}){% endmacro %} +{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.get_instance_type_string() | as_unembedded_code }}){% endmacro %} -{% macro transform(property, source, destination, declare_type=True, multipart=False) %} -{% set type_string = property.get_type_string(json=True) %} +{% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} +{% set type_string = property.get_type_string(json=True) | as_unembedded_code %} {% if property.required %} {{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = {{ source }} {%- else %} -{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET +{% if not skip_unset %}{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET{% endif +%} if not isinstance({{ source }}, Unset): {{ destination }} = {{ source }} {% endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% set transformed = "(None, str(" + source + ").encode(), \"text/plain\")" %} -{% set type_string = "Union[Unset, tuple[None, bytes, str]]" %} -{% if property.required %} -{{ destination }} = {{ transformed }} -{%- else %} -{{ destination }}: {{ type_string }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} -{% endif %} +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{ source }}).encode(), "text/plain"))) {% endmacro %} {% macro transform_header(source) %} diff --git a/openapi_python_client/templates/property_templates/model_property.py.jinja b/openapi_python_client/templates/property_templates/model_property.py.jinja index 308b7478b..629a63c82 100644 --- a/openapi_python_client/templates/property_templates/model_property.py.jinja +++ b/openapi_python_client/templates/property_templates/model_property.py.jinja @@ -10,38 +10,29 @@ {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, dict){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% set transformed = source + ".to_dict()" %} -{% set type_string = property.get_type_string(json=True) %} +{% set type_string = property.get_type_string(json=True) | as_unembedded_code %} {% if property.required %} {{ destination }} = {{ transformed }} {%- else %} -{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET +{% if not skip_unset %}{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET{% endif %} + if not isinstance({{ source }}, Unset): {{ destination }} = {{ transformed }} {%- endif %} {% endmacro %} -{% macro transform_multipart_body(property, source, destination) %} -{% set transformed = source + ".to_multipart()" %} -{% set type_string = property.get_type_string(multipart=True) %} +{% macro transform_multipart_body(property) %} +{% set transformed = property.python_name + ".to_multipart()" %} {% if property.required %} -{{ destination }} = {{ transformed }} +_kwargs["files"] = {{ transformed }} {%- else %} -{{ destination }}: {{ type_string }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} +if not isinstance({{ property.python_name }}, Unset): + _kwargs["files"] = {{ transformed }} {%- endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% set transformed = "(None, json.dumps(" + source + ".to_dict()" + ").encode(), 'application/json')" %} -{% set type_string = property.get_type_string(multipart=True) %} -{% if property.required %} -{{ destination }} = {{ transformed }} -{%- else %} -{{ destination }}: {{ type_string }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = {{ transformed }} -{%- endif %} +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, json.dumps( {{source}}.to_dict()).encode(), "application/json"))) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/property_macros.py.jinja b/openapi_python_client/templates/property_templates/property_macros.py.jinja index 52e1d41bc..238f8560b 100644 --- a/openapi_python_client/templates/property_templates/property_macros.py.jinja +++ b/openapi_python_client/templates/property_templates/property_macros.py.jinja @@ -3,7 +3,7 @@ {{ property.python_name }} = {{ construct_function(property, source) }} {% else %}{# Must be non-required #} _{{ property.python_name }} = {{ source }} -{{ property.python_name }}: {{ property.get_type_string() }} +{{ property.python_name }}: {{ property.get_type_string() | as_unembedded_code }} {% if not property.required %} if isinstance(_{{ property.python_name }}, Unset): {{ property.python_name }} = UNSET diff --git a/openapi_python_client/templates/property_templates/union_property.py.jinja b/openapi_python_client/templates/property_templates/union_property.py.jinja index dbf7ee9dc..45f350f5e 100644 --- a/openapi_python_client/templates/property_templates/union_property.py.jinja +++ b/openapi_python_client/templates/property_templates/union_property.py.jinja @@ -1,10 +1,10 @@ {% macro construct(property, source) %} -def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_string() }}: - {% if "None" in property.get_type_strings_in_union(json=True, multipart=False) %} +def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_string() | as_unembedded_code }}: + {% if PythonCode("None") in property.get_type_strings_in_union(json=True) %} if data is None: return data {% endif %} - {% if "Unset" in property.get_type_strings_in_union(json=True, multipart=False) %} + {% if PythonCode("Unset") in property.get_type_strings_in_union(json=True) %} if isinstance(data, Unset): return data {% endif %} @@ -21,7 +21,7 @@ def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_stri raise TypeError() {{ inner_template.construct(inner_property, "data") | indent(8) }} return {{ inner_property.python_name }} - except: # noqa: E722 + except (TypeError, ValueError, AttributeError, KeyError): pass {% else %}{# Don't do try/except for the last one nor any properties with no type checking #} {% if inner_template.check_type_for_construct %} @@ -33,17 +33,17 @@ def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_stri {% endif %} {% endfor %} {% if ns.contains_unmodified_properties %} - return cast({{ property.get_type_string() }}, data) + return cast({{ property.get_type_string() | as_unembedded_code }}, data) {% endif %} {{ property.python_name }} = _parse_{{ property.python_name }}({{ source }}) {% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% set ns = namespace(contains_properties_without_transform = false, contains_modified_properties = not property.required, has_if = false) %} -{% if declare_type %}{{ destination }}: {{ property.get_type_string(json=True, multipart=False) }}{% endif %} +{% if declare_type %}{{ destination }}: {{ property.get_type_string(json=True) | as_unembedded_code }}{% endif %} -{% if not property.required %} +{% if not property.required and not skip_unset %} if isinstance({{ source }}, Unset): {{ destination }} = UNSET {% set ns.has_if = true %} @@ -57,10 +57,10 @@ if isinstance({{ source }}, Unset): {% set ns.contains_modified_properties = true %} {% endif %} {% if not ns.has_if %} -if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): +if isinstance({{ source }}, {{ inner_property.get_instance_type_string() | as_unembedded_code }}): {% set ns.has_if = true %} {% elif not loop.last or ns.contains_properties_without_transform %} -elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): +elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() | as_unembedded_code }}): {% else %} else: {% endif %} @@ -75,25 +75,28 @@ else: {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% set ns = namespace(has_if = false) %} -{{ destination }}: {{ property.get_type_string(json=False, multipart=True) }} - -{% if not property.required %} -if isinstance({{ source }}, Unset): - {{ destination }} = UNSET - {% set ns.has_if = true %} +{% macro instance_check(inner_property, source) %} +{% if inner_property.get_instance_type_string() == PythonCode("None") %} +if {{ source }} is None: +{% else %} +if isinstance({{ source }}, {{ inner_property.get_instance_type_string() | as_unembedded_code }}): {% endif %} +{% endmacro %} + +{% macro multipart(property, source, destination) %} +{% set ns = namespace(has_if = false) %} {% for inner_property in property.inner_properties %} {% if not ns.has_if %} -if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): +{{ instance_check(inner_property, source) }} {% set ns.has_if = true %} {% elif not loop.last %} -elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): + +el{{ instance_check(inner_property, source) }} {% else %} + else: {% endif %} {% import "property_templates/" + inner_property.template as inner_template %} - {{ inner_template.transform_multipart(inner_property, source, destination) | indent(4) | trim }} -{% endfor %} + {{ inner_template.multipart(inner_property, source, destination) | indent(4) | trim }} +{%- endfor -%} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/uuid_property.py.jinja b/openapi_python_client/templates/property_templates/uuid_property.py.jinja index 1f91c1e3a..1f2910008 100644 --- a/openapi_python_client/templates/property_templates/uuid_property.py.jinja +++ b/openapi_python_client/templates/property_templates/uuid_property.py.jinja @@ -10,29 +10,24 @@ UUID({{ source }}) {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, skip_unset=False) %} {% set transformed = "str(" + source + ")" %} {% if property.required %} {{ destination }} = {{ transformed }} {%- else %} -{% if declare_type %} -{% set type_annotation = property.get_type_string(json=True) %} +{% if not skip_unset %} + {% if declare_type %} + {% set type_annotation = property.get_type_string(json=True) | as_unembedded_code %} {{ destination }}: {{ type_annotation }} = UNSET -{% else %} + {% else %} {{ destination }} = UNSET + {% endif %} {% endif %} if not isinstance({{ source }}, Unset): {{ destination }} = {{ transformed }} {%- endif %} {% endmacro %} -{% macro transform_multipart(property, source, destination) %} -{% if property.required %} -{{ destination }} = str({{ source }}) -{%- else %} -{% set type_annotation = property.get_type_string(json=True) | replace("str", "bytes") %} -{{ destination }}: {{ type_annotation }} = UNSET -if not isinstance({{ source }}, Unset): - {{ destination }} = str({{ source }}) -{%- endif %} +{% macro multipart(property, source, name) %} +files.append(({{ name }}, (None, str({{ source }}), "text/plain"))) {% endmacro %} diff --git a/openapi_python_client/templates/pyproject.toml.jinja b/openapi_python_client/templates/pyproject.toml.jinja index e9344b436..9f21f8043 100644 --- a/openapi_python_client/templates/pyproject.toml.jinja +++ b/openapi_python_client/templates/pyproject.toml.jinja @@ -1,49 +1,9 @@ -{% set poetry = meta == "poetry" %} -{% set pdm = meta == "pdm" %} -{% if poetry or pdm %} -{% if poetry %}[tool.poetry] -{% elif pdm %}[project] +{% if meta == "poetry" %} +{% include "pyproject_poetry.toml.jinja" %} +{% elif meta == "pdm" %} +{% include "pyproject_pdm.toml.jinja" %} +{% elif meta == "uv" %} +{% include "pyproject_uv.toml.jinja" %} {% endif %} -name = "{{ project_name }}" -version = "{{ package_version }}" -description = "{{ package_description }}" -authors = [] -readme = "README.md" -{% if pdm %}requires-python = ">=3.9,<4.0"{% endif %} -{% if poetry %} -packages = [ - {include = "{{ package_name }}"}, -] -include = ["CHANGELOG.md", "{{ package_name }}/py.typed"] -{% endif %} - -{% if pdm %} -dependencies = [ - "httpx>=0.20.0,<0.29.0", - "attrs>=22.2.0", - "python-dateutil>=2.8.0", -] - -[tool.pdm] -distribution = true -{% endif %} -{% if poetry %} - -[tool.poetry.dependencies] -python = "^3.9" -httpx = ">=0.20.0,<0.29.0" -attrs = ">=22.2.0" -python-dateutil = "^2.8.0" -{% endif %} - -[build-system] -{% if poetry %} -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" -{% elif pdm %} -requires = ["pdm-backend"] -build-backend = "pdm.backend" -{% endif %} -{% endif %}{# poetry or pdm #} {% include "pyproject_ruff.toml.jinja" %} diff --git a/openapi_python_client/templates/pyproject_pdm.toml.jinja b/openapi_python_client/templates/pyproject_pdm.toml.jinja new file mode 100644 index 000000000..c3cea3b08 --- /dev/null +++ b/openapi_python_client/templates/pyproject_pdm.toml.jinja @@ -0,0 +1,18 @@ +[project] +name = "{{ project_name }}" +version = "{{ package_version | in_double_quote_literal }}" +description = "{{ package_description | in_double_quote_literal }}" +authors = [] +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "httpx2>=2.13.1,<3", + "attrs>=22.2.0", +] + +[tool.pdm] +distribution = true + +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" diff --git a/openapi_python_client/templates/pyproject_poetry.toml.jinja b/openapi_python_client/templates/pyproject_poetry.toml.jinja new file mode 100644 index 000000000..ade407c92 --- /dev/null +++ b/openapi_python_client/templates/pyproject_poetry.toml.jinja @@ -0,0 +1,19 @@ +[tool.poetry] +name = "{{ project_name }}" +version = "{{ package_version | in_double_quote_literal }}" +description = "{{ package_description | in_double_quote_literal }}" +authors = [] +readme = "README.md" +packages = [ + { include = "{{ package_name }}" }, +] +include = ["{{ package_name }}/py.typed"] + +[tool.poetry.dependencies] +python = "^3.11" +httpx2 = ">=2.13.1,<3" +attrs = ">=22.2.0" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/openapi_python_client/templates/pyproject_uv.toml.jinja b/openapi_python_client/templates/pyproject_uv.toml.jinja new file mode 100644 index 000000000..e8fc7632a --- /dev/null +++ b/openapi_python_client/templates/pyproject_uv.toml.jinja @@ -0,0 +1,19 @@ +[project] +name = "{{ project_name }}" +version = "{{ package_version | in_double_quote_literal }}" +description = "{{ package_description | in_double_quote_literal }}" +authors = [] +requires-python = ">=3.11" +readme = "README.md" +dependencies = [ + "httpx2>=2.13.1,<3", + "attrs>=22.2.0", +] + +[tool.uv.build-backend] +module-name = "{{ package_name }}" +module-root = "" + +[build-system] +requires = ["uv_build>=0.13.0,<0.14.0"] +build-backend = "uv_build" diff --git a/openapi_python_client/templates/setup.py.jinja b/openapi_python_client/templates/setup.py.jinja index c7c1a5a94..255629213 100644 --- a/openapi_python_client/templates/setup.py.jinja +++ b/openapi_python_client/templates/setup.py.jinja @@ -7,12 +7,12 @@ long_description = (here / "README.md").read_text(encoding="utf-8") setup( name="{{ project_name }}", - version="{{ package_version }}", - description="{{ package_description }}", + version="{{ package_version | in_double_quote_literal }}", + description="{{ package_description | in_double_quote_literal }}", long_description=long_description, long_description_content_type="text/markdown", packages=find_packages(), - python_requires=">=3.9, <4", - install_requires=["httpx >= 0.20.0, < 0.29.0", "attrs >= 22.2.0", "python-dateutil >= 2.8.0, < 3"], + python_requires=">=3.11, <4", + install_requires=["httpx2 >= 2.13.1, < 3", "attrs >= 22.2.0"], package_data={"{{ package_name }}": ["py.typed"]}, ) diff --git a/openapi_python_client/templates/str_enum.py.jinja b/openapi_python_client/templates/str_enum.py.jinja index e0da5ed0f..ba47032a5 100644 --- a/openapi_python_client/templates/str_enum.py.jinja +++ b/openapi_python_client/templates/str_enum.py.jinja @@ -1,6 +1,6 @@ -from enum import Enum +from enum import StrEnum -class {{ enum.class_info.name }}(str, Enum): +class {{ enum.class_info.name }}(StrEnum): {% for key, value in enum.values|dictsort(true) %} {{ key }} = "{{ value }}" {% endfor %} diff --git a/openapi_python_client/templates/types.py.jinja b/openapi_python_client/templates/types.py.jinja index 6e0d6206c..2047c19c1 100644 --- a/openapi_python_client/templates/types.py.jinja +++ b/openapi_python_client/templates/types.py.jinja @@ -1,8 +1,8 @@ """ Contains some shared types for properties """ -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from http import HTTPStatus -from typing import BinaryIO, Generic, Optional, TypeVar, Literal +from typing import BinaryIO, Generic, TypeVar, Literal, IO from attrs import define @@ -14,20 +14,26 @@ class Unset: UNSET: Unset = Unset() -{# Used as `FileProperty._json_type_string` #} -FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]] - +# The types that `httpx2.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] @define class File: """ Contains information for file uploads """ payload: BinaryIO - file_name: Optional[str] = None - mime_type: Optional[str] = None + file_name: str | None = None + mime_type: str | None = None - def to_tuple(self) -> FileJsonType: - """ Return a tuple representation that httpx will accept for multipart/form-data """ + def to_tuple(self) -> FileTypes: + """ Return a tuple representation that httpx2 will accept for multipart/form-data """ return self.file_name, self.payload, self.mime_type @@ -41,7 +47,7 @@ class Response(Generic[T]): status_code: HTTPStatus content: bytes headers: MutableMapping[str, str] - parsed: Optional[T] + parsed: T | None -__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"] +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/openapi_python_client/utils.py b/openapi_python_client/utils.py deleted file mode 100644 index 15e8c9eec..000000000 --- a/openapi_python_client/utils.py +++ /dev/null @@ -1,122 +0,0 @@ -from __future__ import annotations - -import builtins -import re -from email.message import Message -from keyword import iskeyword -from typing import Any - -from .config import Config - -DELIMITERS = r"\. _-" - - -class PythonIdentifier(str): - """A snake_case string which has been validated / transformed into a valid identifier for Python""" - - def __new__(cls, value: str, prefix: str, skip_snake_case: bool = False) -> PythonIdentifier: - new_value = sanitize(value) - if not skip_snake_case: - new_value = snake_case(new_value) - new_value = fix_reserved_words(new_value) - - if not new_value.isidentifier() or value.startswith("_"): - new_value = f"{prefix}{new_value}" - return str.__new__(cls, new_value) - - def __deepcopy__(self, _: Any) -> PythonIdentifier: - return self - - -class ClassName(str): - """A PascalCase string which has been validated / transformed into a valid class name for Python""" - - def __new__(cls, value: str, prefix: str) -> ClassName: - new_value = fix_reserved_words(pascal_case(sanitize(value))) - - if not new_value.isidentifier(): - value = f"{prefix}{new_value}" - new_value = fix_reserved_words(pascal_case(sanitize(value))) - return str.__new__(cls, new_value) - - def __deepcopy__(self, _: Any) -> ClassName: - return self - - -def sanitize(value: str) -> str: - """Removes every character that isn't 0-9, A-Z, a-z, or a known delimiter""" - return re.sub(rf"[^\w{DELIMITERS}]+", "", value) - - -def split_words(value: str) -> list[str]: - """Split a string on words and known delimiters""" - # We can't guess words if there is no capital letter - if any(c.isupper() for c in value): - value = " ".join(re.split("([A-Z]?[a-z]+)", value)) - return re.findall(rf"[^{DELIMITERS}]+", value) - - -RESERVED_WORDS = (set(dir(builtins)) | {"self", "true", "false", "datetime"}) - { - "id", -} - - -def fix_reserved_words(value: str) -> str: - """ - Using reserved Python words as identifiers in generated code causes problems, so this function renames them. - - Args: - value: The identifier to-be that should be renamed if it's a reserved word. - - Returns: - `value` suffixed with `_` if it was a reserved word. - """ - if value in RESERVED_WORDS or iskeyword(value): - return f"{value}_" - return value - - -def snake_case(value: str) -> str: - """Converts to snake_case""" - words = split_words(sanitize(value)) - return "_".join(words).lower() - - -def pascal_case(value: str) -> str: - """Converts to PascalCase""" - words = split_words(sanitize(value)) - capitalized_words = (word.capitalize() if not word.isupper() else word for word in words) - return "".join(capitalized_words) - - -def kebab_case(value: str) -> str: - """Converts to kebab-case""" - words = split_words(sanitize(value)) - return "-".join(words).lower() - - -def remove_string_escapes(value: str) -> str: - """Used when parsing string-literal defaults to prevent escaping the string to write arbitrary Python - - **REMOVING OR CHANGING THE USAGE OF THIS FUNCTION HAS SECURITY IMPLICATIONS** - - See Also: - - https://github.com/openapi-generators/openapi-python-client/security/advisories/GHSA-9x4c-63pf-525f - """ - return value.replace('"', r"\"") - - -def get_content_type(content_type: str, config: Config) -> str | None: - """ - Given a string representing a content type with optional parameters, returns the content type only - """ - content_type = config.content_type_overrides.get(content_type, content_type) - message = Message() - message.add_header("Content-Type", content_type) - - parsed_content_type = message.get_content_type() - if not content_type.startswith(parsed_content_type): - # Always defaults to `text/plain` if it's not recognized. We want to return an error, not default. - return None - - return parsed_content_type diff --git a/pdm.lock b/pdm.lock index cdcfded1f..dbcd48e64 100644 --- a/pdm.lock +++ b/pdm.lock @@ -4,78 +4,126 @@ [metadata] groups = ["default", "dev"] strategy = ["inherit_metadata"] -lock_version = "4.5.0" -content_hash = "sha256:7f8035cfb12eec0bca9053eb34cc6b70a1cfbf464921c1abb3ae3c3e63cfeab4" +lock_version = "4.5.1" +content_hash = "sha256:c5878d921aec4583fa4ebeecc159abecf4dc07f3de1196996f3622e7ce8aeddf" [[metadata.targets]] -requires_python = "~=3.9" +requires_python = "~=3.11" + +[[package]] +name = "annotated-doc" +version = "0.0.5" +requires_python = ">=3.9" +summary = "Document parameters, class attributes, return types, and variables inline, with Annotated." +groups = ["default"] +files = [ + {file = "annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101"}, + {file = "annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb"}, +] [[package]] name = "annotated-types" -version = "0.7.0" -requires_python = ">=3.8" +version = "0.8.0" +requires_python = ">=3.10" summary = "Reusable constraint types to use with typing.Annotated" groups = ["default"] -dependencies = [ - "typing-extensions>=4.0.0; python_version < \"3.9\"", -] 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"}, + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, ] [[package]] name = "anyio" -version = "4.7.0" -requires_python = ">=3.9" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" +version = "4.15.1" +requires_python = ">=3.10" +summary = "High-level concurrency and networking framework on top of asyncio or Trio" groups = ["default"] +marker = "sys_platform != \"emscripten\"" dependencies = [ "exceptiongroup>=1.0.2; python_version < \"3.11\"", "idna>=2.8", - "sniffio>=1.1", - "typing-extensions>=4.5; python_version < \"3.13\"", -] -files = [ - {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, - {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, + "typing-extensions>=4.16.0; python_version < \"3.15\"", ] - -[[package]] -name = "attrs" -version = "24.3.0" -requires_python = ">=3.8" -summary = "Classes Without Boilerplate" -groups = ["default"] files = [ - {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, - {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, + {file = "anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101"}, + {file = "anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94"}, ] [[package]] -name = "certifi" -version = "2024.12.14" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." -groups = ["default"] +name = "ast-serialize" +version = "0.13.1" +requires_python = ">=3.7" +summary = "Python bindings for mypy AST serialization" +groups = ["dev"] files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, + {file = "ast_serialize-0.13.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3e129d44b0cffbea24a7f40117ce49e30993c3d119e584d4817ebd6f87246456"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:523d45bab16ddaf1399c04c4fe81bbf180f81e33c2f7bcbe76be56b0f7311ab4"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:638333fb8ae72c62753f9e794fda20290883ddc65b0d85e26e08f8bb227567ad"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc34c670747d85d4232834665e247009413eaebe47c69eaf60a5c439aaecaed9"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c8ebd419811c435812d242fb2a173e57dfede198c9ac9a0a584666ebc9b9331"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8899c038b1c15ae8b98e379057fb3ef398ceb0b21d1737a8156dd5a0923b7e51"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e14c4de98f965647a74e6eaf37e20207124e45451ccd36e84a6f6ebab45e0372"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3e8a3eff69af640db985360fef35073fb46956dfc3e5c614cf00920d2746dd"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:75d7750fe98f6f35a96c43aba8d98a2e623e22c778a0c17ab9ede92eb10f5d82"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e8ddd63e80fc280c2f790157386f27f9b9f6ccb9c4f8e803b863cbfb085769"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9221e09496fb942901993c1812ee87950cc0ecbb514c7c0f1f5fa6d78f701ed8"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7a5c38bdb93669c596f94fb7204dd6ebe4fadb19058ac9a2faf2948c2a175f96"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:acf4e9f055d75058ab01536da330648746f2f82b7697a85230547f2d5ebe895c"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87427913c325adc811e81e7c9a757b061f47ef1e29114ac479aa0953a2687a98"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ad88921f95b92bc144d9ad059a0e16f8c1ae69f5239f2d3c6536fd2774f73dfe"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f8b17e2921e56621bcb46792bd3e0b82ca3b85de78d5936e7b8174ba1758b0aa"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win32.whl", hash = "sha256:2f9a9ce8b0994f757b941f7f7fe384a6666b4162b941ebc5c248081baea18e47"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5d99910e6b9a9b9360bcca2ae99aee36cc912ce058df2a57d1c33099b6cb2593"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a789cbe14d0a0e9ec9b32a456e5abc81ad8799479c3cfd88a29e3442262aebe6"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:23d768369b89d551077dc173da6892fad00443338417523b6732f4e8ba4a7ed5"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:68f8829d114dab6c9db4a8bbb8d0e342958faee1b5e15a29022678df639960f8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e385b17d5bde892a6184003a9aff3518387908b73f8f5ea539313c07c8c0ede"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02686b281ec9b8e9614f6f0db27e12e4cac515f336c5d05b678bc9eb73ff2af5"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8ab2c2585b2e22e48cd4aff2136fce4eab4ced569b85f150ef9a5a5d6f34d49"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c652d9f5d52d8b81eff3ff2c95c9dd0f281de45f6ad5d3fa95527550c396454a"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121e1d08d58b75c15491cc3003609cbad71ac2b0016043c36d30a6b2032b6605"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:30cf89b6a6844542d116c758725d13de5b97b9410a51ee577715f761270d0c1c"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:732aaae9e498c973fc63fd7f308009b57c4afa6ff59d0b674d894e57d0bca01a"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d9c1a920b2aa0c1e016bed7721689717034ca0dba64ac3a3ff243b003d4e8dd8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:847cad4c22953823f5e76d0b126c6f9da670cfa45b1df41b9324e22189d77611"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:aca07de3ca8e3d9f2b716c3c31f4f384fe4bd2d03e09912417cff7a3e08036f7"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:5db12d843d931b9d1f1519ce66b9cbfe72d8563b75c1204d255322254d1964dd"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:fb8021f4dc3296de18737e3b8b34994411da725374d74a0b9eb32a738f2a23d0"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:0d764913374ba21df8cd618045b371ed104ce55c6276de7c59e554a4f5ba26fd"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:2bad302ae2427454ef9dab71bd5c3ad67188860c39d36daee38f72031edb39c4"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:032b09d86c785c3984e975378b7861f2a68ac44e22ba7b07b93a536b2524eeb8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:929eda0c419e622ab49df3a4c45f81c57dcc10874bbad55d0470a4a03324dac3"}, + {file = "ast_serialize-0.13.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bf53a3ba1cbe45399fc1e4ccd684b8f2a11cca450f89706b79122f478534c6f5"}, + {file = "ast_serialize-0.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4bcf1d5f4eefecaa325a34ecf079b2741189e7d0821523f92cb343a389a67489"}, + {file = "ast_serialize-0.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1b158f1c9e3a0d386913924c57995d6df78224868a4d0f215bdd41587c97b34a"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a6201b69ce42e919f2fd9a559138d5410b5d8afff7c3a6b1f500b0481ff7d8"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eb0be64c9b8f1ed26637eef345abe75ea90351c92c85a8aec7838b3af971011"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2427bddfca208e84de700044fadf9aef2769a70bae740a1bdabb89ee9e1117"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88df960e67184a71874e5b2ee014f87d1254a0dbc1f903c7bd7b1ab7733ecaef"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f99786e0d62082a5b121b3437f3167562133711df22128de1e9886f57f0b226"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:a01b515b25a03184eff4acef25b981ae007916379682e908fc8785b342a0bf3e"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29422d7bcd9e7f39411571c36cec45ce8a7a9b3ad4b2e7aa64497c15a9e397f6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ffb0f548a7dc126067fdfb7b2325ab5dfcdeecc29faf5e0f0461c96d6ebfa3d"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2876444a5f87a7c76f371af3c07c2fda563a08bc8af52568df812e8aba4bf568"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:885a62ec6481ce58ab24c6357360d7a82023b62dd7e5e7ceccf59e9662a3272b"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2015fd0165e463aac3b1aadc6a1fa1e5820c14ca950821f4fe02f8a3f6a388d6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:eb41e1906502ac80d24ff343f45008552a3f5b5158607959fe6a07dbfc6eb037"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ca9acc67f796aa2554fa3037dccc022a4b08b38b2ad153edfac9c9bb7cb1f3c6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win32.whl", hash = "sha256:4f98da562e5901d69c7dd30885594c5438f859edf354e9ac7517d188fb026c3a"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:ddf7cbc9c8abf70102c4a088efe154bb86bfb825b0a10e556d2b15972cf99bd7"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win_arm64.whl", hash = "sha256:48f11d96c9ec71119612f8e79a8c55c12f2b136716d68347433c561d27a25f43"}, + {file = "ast_serialize-0.13.1.tar.gz", hash = "sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025"}, ] [[package]] -name = "click" -version = "8.1.8" -requires_python = ">=3.7" -summary = "Composable command line interface toolkit" +name = "attrs" +version = "26.1.0" +requires_python = ">=3.9" +summary = "Classes Without Boilerplate" groups = ["default"] -dependencies = [ - "colorama; platform_system == \"Windows\"", - "importlib-metadata; python_version < \"3.8\"", -] files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] [[package]] @@ -92,234 +140,443 @@ files = [ [[package]] name = "coverage" -version = "7.6.9" -requires_python = ">=3.9" +version = "7.17.0" +requires_python = ">=3.11" summary = "Code coverage measurement for Python" groups = ["dev"] files = [ - {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, - {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, - {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, - {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, - {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, - {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, - {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, - {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, - {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, - {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, - {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, - {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, - {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, - {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, - {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, - {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, + {file = "coverage-7.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99a7b390cbccc6bf029ca4da3f0f607c218bb4085e9cd6b58a90d14ca12993c3"}, + {file = "coverage-7.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f72640e02a470b57b15cd655820133f6892760209283a363a60e2df932d5f0fe"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2dc9233f39149b5a4f4aba17a4b6bc5cca22e52cfc4b06fdb07d014439b8b2ea"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e8113d3852e9377421a61de1917df27b3abb25404a4ecf69f9e64612c4345b5"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86a39539d10d666bb819c16c94d7edfc1aa2e0531b937f7c549b83d15177c6e"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:559f748446ee799803686b5784e3deb54c70d655ac386a860e06b11672504ba7"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d65c9e8e257f48a654908005046fbf1bc48d9540ae3776ad08c0acd010e10c"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b7fa0997b8eba7378ac66f31ec7f70164490353ab79b9667d37c0d365e50177"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6ae0453a3390e67f7bfbe34417bbdf5b4ebfc21db23fb446a0a41115eeeca6ca"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:96151262832f3c230ebae23b2d6911ad549e1738465946c97004913ff06112ba"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cd7397de6cc782f00fe5ac1d75e9b306bcfa49c8b6b8dd6ed1f7709393ad40b3"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d1fdf82c6fea57b5e9dada509f9ce9638c8334664e2c98c7f6f1ccf3d143ad2"}, + {file = "coverage-7.17.0-cp311-cp311-win32.whl", hash = "sha256:38c86e5d74bbfd88a393fa3f91257c1225c63aa6db6d5918de3e49ab04f91c6e"}, + {file = "coverage-7.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:2ed3097c6817bc1a7fc8ae0a6131db74aa1150d863cccc308237ff362d3226b2"}, + {file = "coverage-7.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:6052e878b628abcd38fa5f2b64abebc686eed4e2267da42434017a9c6d24f241"}, + {file = "coverage-7.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0cfdc938ca3319d873d1f1252e60620a39aee4ebf9dbd7cc58238109ebda9c51"}, + {file = "coverage-7.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d523da3c2e4bca49417768bcdb1a443c5b359dc44b8b93a3102e118fce912c40"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:adbf3450c0f3f2b2854e282a8758f2b3eddf787eeff24ec9c9bc2cbc38328cc1"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85932227e9dd38e62cd29c17fefdfecfb919eead6af1e3259522d03cf85de983"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f5477aac9c9e4a0a641c55f8f8fddd7f3bd91176c912c50f40ffced96b4034"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b8bb651a4406fb6a983a905309fec1783443c65e1771b41e92740a0f4a1176b"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0edeb6594f080c8de1263cccb239cb316ef77aeb959e2e0f38f2953fcc72a49e"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da94ec0484e84fce5c7bf8d0f54b97767a90d992653a129b35634699d516e0a5"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a25cc5e8c23a590815c6a7aa43624234d25cb23b21b484adb8a0daed59860db5"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db645c81cf4d6bcb392ea1894aad65d55d1588a8472c834b25ca575da3029a56"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6a80a11b90c4a03df1502304cf24ff953255b6b5d076ef79066ecf88baaff31f"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4eaf11d6b0f2147f46cfbf06fe7edcca6f095d0120da00ddc2458995ee869fc"}, + {file = "coverage-7.17.0-cp312-cp312-win32.whl", hash = "sha256:912a29f338798541d795e2e25903c6fe7a99dee70a40fe4706a9a124649b523e"}, + {file = "coverage-7.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:88a1a9fb0075b83389dd9b403bb3fce976031b3c2aa0e05e3e57143cf639de69"}, + {file = "coverage-7.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:2c680e838ff9e68d491158625ca2c1ddb17d78d791bba96a31f8b839b4990438"}, + {file = "coverage-7.17.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:84538c040f4e6214f663459d89372ce4e6b2bbdd9d397ef0dca7b3ecd56b8eb2"}, + {file = "coverage-7.17.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:6abfb6a4cf1cb1b67bc8fccdde177e2d2b781d0d34449e7078156547025140e5"}, + {file = "coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ef65e96f0fc1a7c8b7644c7011d47a8228555e7888ac4d6246f715a7f3d7c398"}, + {file = "coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:55939ea4c097fda2edabf80fac5e49bef15597b950a9f29fc56292a895245feb"}, + {file = "coverage-7.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bfb6f868af552c0ceb2d19220b8cbcc2be096ca26163ac30e682a13041433429"}, + {file = "coverage-7.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8d823d66afd59566030539c49c7a6acaf3083996682be2dda760c63555842094"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50678474e714c684b82c108e63fbb8f1bcf1266d8293a49980f1d08526f093b5"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:045c1eeb19fb4eae949850a761102fffb3a5f638b97f977912f95843e54f6669"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfb12891a773cbc2132349c77c690a045a882fe3f98b4bda50dfabb01893f9bd"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3067608b4429629f640c1b23d2f69b05d2fd45e00ebb6ee9e8fad220a6c11722"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1762e85cbbe3b5ff9c6dcbcc1b89d5c72320094292b7b9572bf882c85e428b7"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62bb77987d3f72a0aaa77c567ee7f9806bc2e748c8f46fafbe1fdb90dd148398"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6cc31f27240d4e080437e3b8229d92f1d809c9a4f233607385373b73974e183e"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61cde5222f1933d476647c5a25ed1a8678b7703098cb5a15f4a0aa39967d8c28"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11f5f7fb2d97a0fe865f5e7bad4eb0619b957637f676bc088d13844adf26ba95"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e09612b1b82c9df1d2b6c85dee88d5aad3e73d77a8ec0b16ac6a7322cbdf5d9"}, + {file = "coverage-7.17.0-cp313-cp313-win32.whl", hash = "sha256:bd3e9eb493631ff1ca34bf5efac25b5c66724f837bf6182c4fc771c55946423d"}, + {file = "coverage-7.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1fc9a7cea899c51d8a53e66c2f082a3d9e461ece32418d3ef0a0b90c32bfd3d"}, + {file = "coverage-7.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:e65a1e61a68493fbfbe44d8bec71b3231d81b9917f62d3dafe0e105072147e83"}, + {file = "coverage-7.17.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:dc5936491e306f2f6f095bd5fba593de11eb0375cee787ca1f245c3a333a523a"}, + {file = "coverage-7.17.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:968c5fb6e0c83cab752d79a70209648ce4d90cae815c60fa229b59e264601062"}, + {file = "coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:12cb8a20ad904a5811e6660d829a586b216e5254b9b34b76dde8a4d65d827dc6"}, + {file = "coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bb5ca66937c55bc85d1a5e9c5900a3e4693a8edf2280a9b3672b7b2661a84b95"}, + {file = "coverage-7.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5cc15faaf318ee5040d835a62c75bc6f122d837274f3b5efd47bf9fbd4ac5b85"}, + {file = "coverage-7.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d6d9d396c8b7bcaf76d774a10140b218111484e54c73a5f2a1b75655381748e"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:42e6f170d1ac04b7eb3363310a6c41cdfd82c1fe6bf9ffbc4a86f380bdf091c7"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:acdb5f3c96046efff9dc6c572a6acb2b8d0da09372d7b9a9ed7249cf7d17434f"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3393209f6894713161ee82a9b0e673e58237e0f9733c6638e7a6c0c620ef0b33"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9494d9e99389d39e6a96227749af898d8924ff591cb63944f8a98e68582bf0e"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:032d4c2db4c677eb71d43f2f8b80bf64e9346f7a3f7b6ebb13f1782f3cc6c274"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4343a9378b964eb3cb80fa90406385de91c7e982a769ca9314d848f93bc0d3f2"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b609df3d6d900464207532c5170a65c6ede8d2e0b91f151f90008a866395cd76"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a01ef92caeedfa4f2ad171a3a8e6fece4565de871d8bca428151bf9929f41048"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a3914e2c98c34b94c26d2add3d2a61ccfd3534fc4eaa879ee290bdbb5184ba6c"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8a72b26dc5c9a9422e45472b1d67ba5caf273c524c8668cc04b23062663755e"}, + {file = "coverage-7.17.0-cp314-cp314-win32.whl", hash = "sha256:03baf0688e14a5a44ffd1eefa8928dfc92b14565e521174e072b31b971aa1657"}, + {file = "coverage-7.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3a4459b0f389dd1e51aac3dbfc4ca35b30cae7dc99caa3c0a086d1997a985709"}, + {file = "coverage-7.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32ee5831b15ba239bef5d272ae586ca8acea4a661b5f2941f5a0d380e518b23"}, + {file = "coverage-7.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9c083e253e96fdeeaf48133fe836cac86ace2db215d235700e7f7532420a200c"}, + {file = "coverage-7.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b88fb4d4d7608cbcebdc7ff7dbffc2115e3b81d6c33a79147cd79d54fcb080f4"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2bf3ee38a40c0570c316b9e7507c472469586906a20f289705898528c31aaea8"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c570293f001ea91598b90e90cb9b98fddee833e90bbede432cbd218331ccd8e"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:791f9ee09814dd30d9562b4f4eb037a371485ec1c830b53999d2320d20c4e764"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7c5672ecf1b497de7a0b37743bef40e3b58e3207e9fe2ed6682cb0420836a88"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:337f87e62e1c5f43c8198d879f677c342d152f84d3f157add12558853cbb8eaf"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c8a58baa8eb6154c6645eadcc19b6c089deab78c24a612a83206f2a8a5992a7c"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb5745048ee20f796b0e3ab61831049e749acff188563e2af32f58ede957ac1b"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:7b5abcae74750302ac4bc77351eaee87c450e4abd3b6225eacde3858e260f8b2"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4e153f0bd6509dbf837fd585f489b3328bfd98f2a718a0b15ae63032148b285c"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e42a835271922fe7bf98c7a05c78d4032f4e52bf5b07fafa1eadfdfc2f0ad22f"}, + {file = "coverage-7.17.0-cp314-cp314t-win32.whl", hash = "sha256:5900055351ec89fdffb34b27898815106529ded29a9496464c1ebd6313fd40fa"}, + {file = "coverage-7.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5ee9c8bd13a59e301dc2aefe7aa8384633406808de051b1982775667094eb673"}, + {file = "coverage-7.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a9cb9a0fb58e6fff5442969af36094ecd6976f4907f179e33b27e15355d8f74"}, + {file = "coverage-7.17.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:adabc1079ba844b096799ba8638d8df434aca1613f95bb7d5791af320cf05559"}, + {file = "coverage-7.17.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:6b4593d2871c4c801656dd5861e759f8f5156febd6b6061b02f6a72d64618b7b"}, + {file = "coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c2c48a36bb970b7265b196032bfd4237c0b373f2b25ef7012d7e905b5b35c19c"}, + {file = "coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f8bc2cd4c6aa58be1ca243fb8cddb6c3c42031fe4403991ad9cf2b3baed03d9f"}, + {file = "coverage-7.17.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:cbf0b83ce927de6053279ba61342d9f8892afde1385fcba9144cd05d4023d109"}, + {file = "coverage-7.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:66efc0fb99cee34626fc17d3adac9c3cd816bfdad563bb1e983c3ef432f566d4"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58a304efdf2253fb2ea03d45d0a04cc174a92a0cd5279c28c5bb893ce7410968"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aed06129a9f530a948928260d7d2d81c47261355b0127e3c782251b38958463c"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6cdb47a9be2d3313829bed1d296086287682a3ee901ff987b3432b45705b771"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:804fbaf81d09b34e2f8b1159d01e211a50713d743d32fd9b2ca6cf0ae507a66f"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2afebb96da9a1bbca142f8cc12320c14e5c215e0a49cded4f191f5a31506a28"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bd04fd23206be9ad3de13b0b39313094fafa66a1f6baf70f5471c004a9fac47d"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:5ebfbc87836529a31d25c2dec1262c38eea852ec1c46c0f690a515a7339d9c5b"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:a59c3b470ac3b2432af3598a996affbeb10236cd899a005c1c4e517fcff8b592"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:7fef12cec4b96f59e09761cbd4fc9bdf4788062d1ae435b920241fc16c8a9549"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a14e8d69464da9cbe1dc29e7116dbd37296d1d51a695327c478e250349085089"}, + {file = "coverage-7.17.0-cp315-cp315-win32.whl", hash = "sha256:9a66074231799d8884d267234d49f60c90b43a945dfe069646b72f3058cd4e67"}, + {file = "coverage-7.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:79cf71e5f9a48877ccfed5baa2c72f45af4feeaa596249e23eb15b473134830e"}, + {file = "coverage-7.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:09e563b49c81cd0a33734e926d7a4a6ef1778d74304cde2dcc3aff8b30ea1036"}, + {file = "coverage-7.17.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a5e2e0625dfc789af0b2e10acfc106b352f314fffbd19e6392a7961c8cbe5d9a"}, + {file = "coverage-7.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:194562ab1eff0d85aee8a17ce4c5c0bc1ebf70a629a7145257123156ad100fd6"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:85682ca89bc4601125dcdff23fa70e6274e13c9ad502fba845e6c274ab2c6569"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0d0fac604a962ff546beb038cba9a5a09d88e6e3f01612fb46f7bd4fe5dc8dca"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72edd4f9483ae30cd1ba4f230687840ef7b58a16afb1c9e81131fd6d64655f36"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91e487ad07c511da77b574820141a7c2da09bc2a4b4090b5be2424295d100697"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1cabb31531002fef5197de5df3c84730aef01e750e25154b9813a2583435aba2"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c9ac886242bbd50fb35a2242ba79555a99891e657da33f98b7ca968e3488a350"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d45cdc495671fc99268ff3653e0da4244972e9fc0e1fc3ea9de18781ad28963a"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:12961f738d717a046d4bf3f8d1727eb3ebe2ed5478ff96cd6233d4dfc39323d3"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1ee0a982b0e1230b3edf768a7dd022bd2377303763895327ac62deb5efacee03"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:460679f53893081edb94eeed011d26be37228ba649b853fd2aed7e4394fb1f35"}, + {file = "coverage-7.17.0-cp315-cp315t-win32.whl", hash = "sha256:600e121e412cd6bdb52ec105b95c6a04da0c9710d1534f6e64a42db667907802"}, + {file = "coverage-7.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c3e11b81faafb1f063248735c3e2c5c4183fc1db2c7f46782170dd7760dffcbc"}, + {file = "coverage-7.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d472ee1c64868a917a47f02ffb4ac8c22b477f7589d57a3eb0c58b1ad4f0137c"}, + {file = "coverage-7.17.0-py3-none-any.whl", hash = "sha256:bac487dce0d2aff28ddf1a533a916f771416eee0e493960f611e49037b878e9d"}, + {file = "coverage-7.17.0.tar.gz", hash = "sha256:cc010a0a7fbdd9bb8bdfd0d3f04a5151275860213cbd7e2b876b29778489c712"}, ] [[package]] name = "coverage" -version = "7.6.9" +version = "7.17.0" extras = ["toml"] -requires_python = ">=3.9" +requires_python = ">=3.11" summary = "Code coverage measurement for Python" groups = ["dev"] dependencies = [ - "coverage==7.6.9", - "tomli; python_full_version <= \"3.11.0a6\"", -] -files = [ - {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, - {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, - {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, - {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, - {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, - {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, - {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, - {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, - {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, - {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, - {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, - {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, - {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, - {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, - {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, - {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -requires_python = ">=3.7" -summary = "Backport of PEP 654 (exception groups)" -groups = ["default", "dev"] -marker = "python_version < \"3.11\"" + "coverage==7.17.0", +] files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "coverage-7.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99a7b390cbccc6bf029ca4da3f0f607c218bb4085e9cd6b58a90d14ca12993c3"}, + {file = "coverage-7.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f72640e02a470b57b15cd655820133f6892760209283a363a60e2df932d5f0fe"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2dc9233f39149b5a4f4aba17a4b6bc5cca22e52cfc4b06fdb07d014439b8b2ea"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e8113d3852e9377421a61de1917df27b3abb25404a4ecf69f9e64612c4345b5"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86a39539d10d666bb819c16c94d7edfc1aa2e0531b937f7c549b83d15177c6e"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:559f748446ee799803686b5784e3deb54c70d655ac386a860e06b11672504ba7"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d65c9e8e257f48a654908005046fbf1bc48d9540ae3776ad08c0acd010e10c"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b7fa0997b8eba7378ac66f31ec7f70164490353ab79b9667d37c0d365e50177"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6ae0453a3390e67f7bfbe34417bbdf5b4ebfc21db23fb446a0a41115eeeca6ca"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:96151262832f3c230ebae23b2d6911ad549e1738465946c97004913ff06112ba"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cd7397de6cc782f00fe5ac1d75e9b306bcfa49c8b6b8dd6ed1f7709393ad40b3"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d1fdf82c6fea57b5e9dada509f9ce9638c8334664e2c98c7f6f1ccf3d143ad2"}, + {file = "coverage-7.17.0-cp311-cp311-win32.whl", hash = "sha256:38c86e5d74bbfd88a393fa3f91257c1225c63aa6db6d5918de3e49ab04f91c6e"}, + {file = "coverage-7.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:2ed3097c6817bc1a7fc8ae0a6131db74aa1150d863cccc308237ff362d3226b2"}, + {file = "coverage-7.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:6052e878b628abcd38fa5f2b64abebc686eed4e2267da42434017a9c6d24f241"}, + {file = "coverage-7.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0cfdc938ca3319d873d1f1252e60620a39aee4ebf9dbd7cc58238109ebda9c51"}, + {file = "coverage-7.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d523da3c2e4bca49417768bcdb1a443c5b359dc44b8b93a3102e118fce912c40"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:adbf3450c0f3f2b2854e282a8758f2b3eddf787eeff24ec9c9bc2cbc38328cc1"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85932227e9dd38e62cd29c17fefdfecfb919eead6af1e3259522d03cf85de983"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f5477aac9c9e4a0a641c55f8f8fddd7f3bd91176c912c50f40ffced96b4034"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b8bb651a4406fb6a983a905309fec1783443c65e1771b41e92740a0f4a1176b"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0edeb6594f080c8de1263cccb239cb316ef77aeb959e2e0f38f2953fcc72a49e"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da94ec0484e84fce5c7bf8d0f54b97767a90d992653a129b35634699d516e0a5"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a25cc5e8c23a590815c6a7aa43624234d25cb23b21b484adb8a0daed59860db5"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db645c81cf4d6bcb392ea1894aad65d55d1588a8472c834b25ca575da3029a56"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6a80a11b90c4a03df1502304cf24ff953255b6b5d076ef79066ecf88baaff31f"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4eaf11d6b0f2147f46cfbf06fe7edcca6f095d0120da00ddc2458995ee869fc"}, + {file = "coverage-7.17.0-cp312-cp312-win32.whl", hash = "sha256:912a29f338798541d795e2e25903c6fe7a99dee70a40fe4706a9a124649b523e"}, + {file = "coverage-7.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:88a1a9fb0075b83389dd9b403bb3fce976031b3c2aa0e05e3e57143cf639de69"}, + {file = "coverage-7.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:2c680e838ff9e68d491158625ca2c1ddb17d78d791bba96a31f8b839b4990438"}, + {file = "coverage-7.17.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:84538c040f4e6214f663459d89372ce4e6b2bbdd9d397ef0dca7b3ecd56b8eb2"}, + {file = "coverage-7.17.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:6abfb6a4cf1cb1b67bc8fccdde177e2d2b781d0d34449e7078156547025140e5"}, + {file = "coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ef65e96f0fc1a7c8b7644c7011d47a8228555e7888ac4d6246f715a7f3d7c398"}, + {file = "coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:55939ea4c097fda2edabf80fac5e49bef15597b950a9f29fc56292a895245feb"}, + {file = "coverage-7.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bfb6f868af552c0ceb2d19220b8cbcc2be096ca26163ac30e682a13041433429"}, + {file = "coverage-7.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8d823d66afd59566030539c49c7a6acaf3083996682be2dda760c63555842094"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50678474e714c684b82c108e63fbb8f1bcf1266d8293a49980f1d08526f093b5"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:045c1eeb19fb4eae949850a761102fffb3a5f638b97f977912f95843e54f6669"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfb12891a773cbc2132349c77c690a045a882fe3f98b4bda50dfabb01893f9bd"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3067608b4429629f640c1b23d2f69b05d2fd45e00ebb6ee9e8fad220a6c11722"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1762e85cbbe3b5ff9c6dcbcc1b89d5c72320094292b7b9572bf882c85e428b7"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62bb77987d3f72a0aaa77c567ee7f9806bc2e748c8f46fafbe1fdb90dd148398"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6cc31f27240d4e080437e3b8229d92f1d809c9a4f233607385373b73974e183e"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61cde5222f1933d476647c5a25ed1a8678b7703098cb5a15f4a0aa39967d8c28"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11f5f7fb2d97a0fe865f5e7bad4eb0619b957637f676bc088d13844adf26ba95"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e09612b1b82c9df1d2b6c85dee88d5aad3e73d77a8ec0b16ac6a7322cbdf5d9"}, + {file = "coverage-7.17.0-cp313-cp313-win32.whl", hash = "sha256:bd3e9eb493631ff1ca34bf5efac25b5c66724f837bf6182c4fc771c55946423d"}, + {file = "coverage-7.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1fc9a7cea899c51d8a53e66c2f082a3d9e461ece32418d3ef0a0b90c32bfd3d"}, + {file = "coverage-7.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:e65a1e61a68493fbfbe44d8bec71b3231d81b9917f62d3dafe0e105072147e83"}, + {file = "coverage-7.17.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:dc5936491e306f2f6f095bd5fba593de11eb0375cee787ca1f245c3a333a523a"}, + {file = "coverage-7.17.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:968c5fb6e0c83cab752d79a70209648ce4d90cae815c60fa229b59e264601062"}, + {file = "coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:12cb8a20ad904a5811e6660d829a586b216e5254b9b34b76dde8a4d65d827dc6"}, + {file = "coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bb5ca66937c55bc85d1a5e9c5900a3e4693a8edf2280a9b3672b7b2661a84b95"}, + {file = "coverage-7.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5cc15faaf318ee5040d835a62c75bc6f122d837274f3b5efd47bf9fbd4ac5b85"}, + {file = "coverage-7.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d6d9d396c8b7bcaf76d774a10140b218111484e54c73a5f2a1b75655381748e"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:42e6f170d1ac04b7eb3363310a6c41cdfd82c1fe6bf9ffbc4a86f380bdf091c7"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:acdb5f3c96046efff9dc6c572a6acb2b8d0da09372d7b9a9ed7249cf7d17434f"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3393209f6894713161ee82a9b0e673e58237e0f9733c6638e7a6c0c620ef0b33"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9494d9e99389d39e6a96227749af898d8924ff591cb63944f8a98e68582bf0e"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:032d4c2db4c677eb71d43f2f8b80bf64e9346f7a3f7b6ebb13f1782f3cc6c274"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4343a9378b964eb3cb80fa90406385de91c7e982a769ca9314d848f93bc0d3f2"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b609df3d6d900464207532c5170a65c6ede8d2e0b91f151f90008a866395cd76"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a01ef92caeedfa4f2ad171a3a8e6fece4565de871d8bca428151bf9929f41048"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a3914e2c98c34b94c26d2add3d2a61ccfd3534fc4eaa879ee290bdbb5184ba6c"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8a72b26dc5c9a9422e45472b1d67ba5caf273c524c8668cc04b23062663755e"}, + {file = "coverage-7.17.0-cp314-cp314-win32.whl", hash = "sha256:03baf0688e14a5a44ffd1eefa8928dfc92b14565e521174e072b31b971aa1657"}, + {file = "coverage-7.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3a4459b0f389dd1e51aac3dbfc4ca35b30cae7dc99caa3c0a086d1997a985709"}, + {file = "coverage-7.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32ee5831b15ba239bef5d272ae586ca8acea4a661b5f2941f5a0d380e518b23"}, + {file = "coverage-7.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9c083e253e96fdeeaf48133fe836cac86ace2db215d235700e7f7532420a200c"}, + {file = "coverage-7.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b88fb4d4d7608cbcebdc7ff7dbffc2115e3b81d6c33a79147cd79d54fcb080f4"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2bf3ee38a40c0570c316b9e7507c472469586906a20f289705898528c31aaea8"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c570293f001ea91598b90e90cb9b98fddee833e90bbede432cbd218331ccd8e"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:791f9ee09814dd30d9562b4f4eb037a371485ec1c830b53999d2320d20c4e764"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7c5672ecf1b497de7a0b37743bef40e3b58e3207e9fe2ed6682cb0420836a88"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:337f87e62e1c5f43c8198d879f677c342d152f84d3f157add12558853cbb8eaf"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c8a58baa8eb6154c6645eadcc19b6c089deab78c24a612a83206f2a8a5992a7c"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb5745048ee20f796b0e3ab61831049e749acff188563e2af32f58ede957ac1b"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:7b5abcae74750302ac4bc77351eaee87c450e4abd3b6225eacde3858e260f8b2"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4e153f0bd6509dbf837fd585f489b3328bfd98f2a718a0b15ae63032148b285c"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e42a835271922fe7bf98c7a05c78d4032f4e52bf5b07fafa1eadfdfc2f0ad22f"}, + {file = "coverage-7.17.0-cp314-cp314t-win32.whl", hash = "sha256:5900055351ec89fdffb34b27898815106529ded29a9496464c1ebd6313fd40fa"}, + {file = "coverage-7.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5ee9c8bd13a59e301dc2aefe7aa8384633406808de051b1982775667094eb673"}, + {file = "coverage-7.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a9cb9a0fb58e6fff5442969af36094ecd6976f4907f179e33b27e15355d8f74"}, + {file = "coverage-7.17.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:adabc1079ba844b096799ba8638d8df434aca1613f95bb7d5791af320cf05559"}, + {file = "coverage-7.17.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:6b4593d2871c4c801656dd5861e759f8f5156febd6b6061b02f6a72d64618b7b"}, + {file = "coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c2c48a36bb970b7265b196032bfd4237c0b373f2b25ef7012d7e905b5b35c19c"}, + {file = "coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f8bc2cd4c6aa58be1ca243fb8cddb6c3c42031fe4403991ad9cf2b3baed03d9f"}, + {file = "coverage-7.17.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:cbf0b83ce927de6053279ba61342d9f8892afde1385fcba9144cd05d4023d109"}, + {file = "coverage-7.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:66efc0fb99cee34626fc17d3adac9c3cd816bfdad563bb1e983c3ef432f566d4"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58a304efdf2253fb2ea03d45d0a04cc174a92a0cd5279c28c5bb893ce7410968"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aed06129a9f530a948928260d7d2d81c47261355b0127e3c782251b38958463c"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6cdb47a9be2d3313829bed1d296086287682a3ee901ff987b3432b45705b771"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:804fbaf81d09b34e2f8b1159d01e211a50713d743d32fd9b2ca6cf0ae507a66f"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2afebb96da9a1bbca142f8cc12320c14e5c215e0a49cded4f191f5a31506a28"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bd04fd23206be9ad3de13b0b39313094fafa66a1f6baf70f5471c004a9fac47d"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:5ebfbc87836529a31d25c2dec1262c38eea852ec1c46c0f690a515a7339d9c5b"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:a59c3b470ac3b2432af3598a996affbeb10236cd899a005c1c4e517fcff8b592"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:7fef12cec4b96f59e09761cbd4fc9bdf4788062d1ae435b920241fc16c8a9549"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a14e8d69464da9cbe1dc29e7116dbd37296d1d51a695327c478e250349085089"}, + {file = "coverage-7.17.0-cp315-cp315-win32.whl", hash = "sha256:9a66074231799d8884d267234d49f60c90b43a945dfe069646b72f3058cd4e67"}, + {file = "coverage-7.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:79cf71e5f9a48877ccfed5baa2c72f45af4feeaa596249e23eb15b473134830e"}, + {file = "coverage-7.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:09e563b49c81cd0a33734e926d7a4a6ef1778d74304cde2dcc3aff8b30ea1036"}, + {file = "coverage-7.17.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a5e2e0625dfc789af0b2e10acfc106b352f314fffbd19e6392a7961c8cbe5d9a"}, + {file = "coverage-7.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:194562ab1eff0d85aee8a17ce4c5c0bc1ebf70a629a7145257123156ad100fd6"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:85682ca89bc4601125dcdff23fa70e6274e13c9ad502fba845e6c274ab2c6569"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0d0fac604a962ff546beb038cba9a5a09d88e6e3f01612fb46f7bd4fe5dc8dca"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72edd4f9483ae30cd1ba4f230687840ef7b58a16afb1c9e81131fd6d64655f36"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91e487ad07c511da77b574820141a7c2da09bc2a4b4090b5be2424295d100697"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1cabb31531002fef5197de5df3c84730aef01e750e25154b9813a2583435aba2"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c9ac886242bbd50fb35a2242ba79555a99891e657da33f98b7ca968e3488a350"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d45cdc495671fc99268ff3653e0da4244972e9fc0e1fc3ea9de18781ad28963a"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:12961f738d717a046d4bf3f8d1727eb3ebe2ed5478ff96cd6233d4dfc39323d3"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1ee0a982b0e1230b3edf768a7dd022bd2377303763895327ac62deb5efacee03"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:460679f53893081edb94eeed011d26be37228ba649b853fd2aed7e4394fb1f35"}, + {file = "coverage-7.17.0-cp315-cp315t-win32.whl", hash = "sha256:600e121e412cd6bdb52ec105b95c6a04da0c9710d1534f6e64a42db667907802"}, + {file = "coverage-7.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c3e11b81faafb1f063248735c3e2c5c4183fc1db2c7f46782170dd7760dffcbc"}, + {file = "coverage-7.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d472ee1c64868a917a47f02ffb4ac8c22b477f7589d57a3eb0c58b1ad4f0137c"}, + {file = "coverage-7.17.0-py3-none-any.whl", hash = "sha256:bac487dce0d2aff28ddf1a533a916f771416eee0e493960f611e49037b878e9d"}, + {file = "coverage-7.17.0.tar.gz", hash = "sha256:cc010a0a7fbdd9bb8bdfd0d3f04a5151275860213cbd7e2b876b29778489c712"}, ] [[package]] name = "h11" -version = "0.14.0" -requires_python = ">=3.7" +version = "0.16.0" +requires_python = ">=3.8" summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" groups = ["default"] -dependencies = [ - "typing-extensions; python_version < \"3.8\"", -] +marker = "sys_platform != \"emscripten\"" files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {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.7" -requires_python = ">=3.8" +name = "httpcore2" +version = "2.16.0" +requires_python = ">=3.11" summary = "A minimal low-level HTTP client." groups = ["default"] +marker = "sys_platform != \"emscripten\"" dependencies = [ - "certifi", - "h11<0.15,>=0.13", + "h11>=0.16", + "truststore>=0.10", ] files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, + {file = "httpcore2-2.16.0-py3-none-any.whl", hash = "sha256:5c4358319f2602d984ad93182c9fee5160f22e9da7babdda870a5e7c4e2dc34f"}, + {file = "httpcore2-2.16.0.tar.gz", hash = "sha256:c72cd0413563b17e1cd8354677dff887466f8ec2551679759e93126534428643"}, ] [[package]] -name = "httpx" -version = "0.28.1" -requires_python = ">=3.8" +name = "httpx2" +version = "2.16.0" +requires_python = ">=3.11" summary = "The next generation HTTP client." groups = ["default"] dependencies = [ - "anyio", - "certifi", - "httpcore==1.*", - "idna", + "anyio>=4.10; sys_platform != \"emscripten\"", + "httpcore2==2.16.0; sys_platform != \"emscripten\"", + "httpx2-jsfetch; sys_platform == \"emscripten\" and python_version >= \"3.12\"", + "idna>=3.18", + "truststore>=0.10; sys_platform != \"emscripten\"", + "typing-extensions>=4.5.0; python_version < \"3.13\"", ] files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, + {file = "httpx2-2.16.0-py3-none-any.whl", hash = "sha256:fd2c5d7909e8d86fe92d53020c310ca0e4e431b838c207c5754b9bbf0308d49a"}, + {file = "httpx2-2.16.0.tar.gz", hash = "sha256:0d54c98128e8f2ff7fbbde0fca8c359e5f032edeaaaac443cdb7f31f685bcd7f"}, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +requires_python = ">=3.12" +summary = "httpx2 transports for Emscripten/Pyodide, backed by the JavaScript fetch API." +groups = ["default"] +marker = "sys_platform == \"emscripten\" and python_version >= \"3.12\"" +files = [ + {file = "httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32"}, + {file = "httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60"}, +] + +[[package]] +name = "hypothesis" +version = "6.170.14" +requires_python = ">=3.11" +summary = "The property-based testing library for Python" +groups = ["dev"] +dependencies = [ + "sortedcontainers<3.0.0,>=2.1.0", +] +files = [ + {file = "hypothesis-6.170.14-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b1c4990f9fafad83cdac33bc564d41066f5824a92ddd45ded7e0e84277a3ec0e"}, + {file = "hypothesis-6.170.14-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:ab414b45a56853fcdd6b602d9c5033e3c7d127ee8b8e7e3397223b746440b37c"}, + {file = "hypothesis-6.170.14-cp311-abi3-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:cceb1b3c19ea0a146c7bb2fb2b44e3ef231bcecd42c97d95154e5d233e209572"}, + {file = "hypothesis-6.170.14-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:376dbf5ffa1e3ad71af2a1af0877176faabe3e63d345324a33c82e408dbfd604"}, + {file = "hypothesis-6.170.14-cp311-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5f0f6f6666fa15ec13ca1d8430f70660fce8b3f4f133e25fdf674be6ad77295e"}, + {file = "hypothesis-6.170.14-cp311-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c4ff70fbb94f54cd35045a40e5a77dd8f876d4cc6a39dcdc4ac1af6f8f5cc6fe"}, + {file = "hypothesis-6.170.14-cp311-abi3-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8ecec6be3a02e22c70bc7dfb15afeeeb708aeccb994c9d57db438199cad9bfbb"}, + {file = "hypothesis-6.170.14-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3bbf06c4f8e3aab37c144fe3bc1d89a051a2dfbefef8c2a25160b11da854f949"}, + {file = "hypothesis-6.170.14-cp311-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:07869d66bf1e92e39447808fcc1977bfd10bfdff4970d6969a49e89a84cf04a7"}, + {file = "hypothesis-6.170.14-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0ce9ddf0d0e99c532c2cf00d97b63316830810d18f11c80c7bcf0f771969f3a4"}, + {file = "hypothesis-6.170.14-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:420b4a77157e94f124498fff639284d514b60926f61925dd7aafb75411e7ce92"}, + {file = "hypothesis-6.170.14-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:2a8ed08715a9a27d3a4730f48c99133ef25b3561d8fe992d1eda91e22cf6be99"}, + {file = "hypothesis-6.170.14-cp311-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:71081b46714e95e9a10d922bedd08d5a6e49835bbc2ef37a81d30d62f8596829"}, + {file = "hypothesis-6.170.14-cp311-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:ce563cb34dedc9163811b03ce05a955eededd6f429db7c80c5c31d5836a9c05a"}, + {file = "hypothesis-6.170.14-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06a752e3e879f1b69dda9ea203d4e8e1562cf72f694513f01c8c99d2e5e3f3cc"}, + {file = "hypothesis-6.170.14-cp311-abi3-win32.whl", hash = "sha256:0f418c6bff7cd8a117dd0f526c20f100c32df1ff9f7bc8e01a5eeb3855278039"}, + {file = "hypothesis-6.170.14-cp311-abi3-win_amd64.whl", hash = "sha256:1e9f7619dff3072685e4f36e8ae7b134a0c1ca41074fc54ade81e5355c72cd8c"}, + {file = "hypothesis-6.170.14-cp311-abi3-win_arm64.whl", hash = "sha256:7dbaec9a62b7792b0b04b7ebc933aeafd3ff019b81d67d763c36ebcf4d56f864"}, + {file = "hypothesis-6.170.14-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6fbe71177ca91a999e8fa644aeaee41622270165c89e4d2b4d02a6ea469de68c"}, + {file = "hypothesis-6.170.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:79319231501689666f1bc58a074fe1673b05d57bdaae35a8603d64dc3f88ee00"}, + {file = "hypothesis-6.170.14-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:61537e5d03fde19b80b3a38ee24ffd1c29f02f049879d3d2b6f48a0fb30f4f54"}, + {file = "hypothesis-6.170.14-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3bf836cb28f07201e0fe7d3cf2ebe1b5f540ab798fd03edb174d98f064bd620a"}, + {file = "hypothesis-6.170.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:149ceb08170a99b2dca0b7d9906e56711b1e574be185f8c233de850a0e7f19d3"}, + {file = "hypothesis-6.170.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71195a69df8a3c9be3cbf17937db0506eab3a15fbb79863a6e3e299ba6712d71"}, + {file = "hypothesis-6.170.14-cp311-cp311-win_amd64.whl", hash = "sha256:dde32e1c901e0532e595608f8bcdbb128f6a5ebe73c592983eec43c57ab9b0c4"}, + {file = "hypothesis-6.170.14-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:76b03ded37a7eb14d01b03edca02b9a6ce6b8ab0ad8d2f9ac5f2e9de6769a33a"}, + {file = "hypothesis-6.170.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6055172afdca41cda869652bd2f2c24ec984005cd16bbec5a8f38483f23b9bc"}, + {file = "hypothesis-6.170.14-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6490c64811a2176a5af9fe5a5d59ab2aa20d35d691328cbb336f6fb398355c4"}, + {file = "hypothesis-6.170.14-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cde7e98694a9baa1d9761c1b595b3d617467c876200c0798a40f2016a0e85304"}, + {file = "hypothesis-6.170.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e39d46bdebb3853fb3000aab3c354e4d780705dc8b2674773ffcdfd04fd5122"}, + {file = "hypothesis-6.170.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1309925a71a2baacb85ed5c20291ddb4b2a4665bd33fb594697463d457374a1b"}, + {file = "hypothesis-6.170.14-cp312-cp312-win_amd64.whl", hash = "sha256:ac98e833bdefbcbebb9094180a7431a8946f1acc91a5b779c814aecca71c7345"}, + {file = "hypothesis-6.170.14-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d5e5affa9e69b8673f15bef86421740945e74f3c31d0817141ba020c7e00705"}, + {file = "hypothesis-6.170.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7ae9ff3220659c4721acb58d34e72227a0af7747fc903ae177dfdb0323796b4"}, + {file = "hypothesis-6.170.14-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:857932ff6bba183af0236f659b042023ab8b4ad58725b9024bcaba941c8cd5d0"}, + {file = "hypothesis-6.170.14-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8126e8208acadfc3c92d0aedb8011cf724fb59e5523ceaca3f4612dcc409e632"}, + {file = "hypothesis-6.170.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d28ab5c5b08b1de11bc022d576b20f9f585f7e7758778295461f80b9222d7a18"}, + {file = "hypothesis-6.170.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:033aca79fd3f9a6e15402dfddbc988141d43eae38d26a5fc554f6879f7e0e5f9"}, + {file = "hypothesis-6.170.14-cp313-cp313-win_amd64.whl", hash = "sha256:03831d833b161b9b7a32f0b607205293954f3a651196c80138804b9479845d71"}, + {file = "hypothesis-6.170.14-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:a2410c5a6dcc011338e53cd381bd32d979b6f06d799c5cf01d76b923f0a0670b"}, + {file = "hypothesis-6.170.14-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcf6303c65e73acf134b7854cbdfec8f2108cfdc84806fa180de04d7946aab0c"}, + {file = "hypothesis-6.170.14-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67d17e8b91d3913e7aa2b1179957df4e0634caea3868b5ba4fe2bf1945334134"}, + {file = "hypothesis-6.170.14-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:96f9f8a684c88ad2280129b28bc609adb6b31cd63b1b83b397b86d552391e947"}, + {file = "hypothesis-6.170.14-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c42d504d9bc0aef71fa88877c68ce202c02242c84f05791be2785e01e3a8699"}, + {file = "hypothesis-6.170.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ad84ca98f38e9f385b0f616df04bea4a9c6849333e221ce9289e9566ccd41058"}, + {file = "hypothesis-6.170.14-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f569bd9745835f9864d53c853e401c2021e796fdbb385497fdae633fe950b6ba"}, + {file = "hypothesis-6.170.14-cp314-cp314-win_amd64.whl", hash = "sha256:31c6a4015c02c45ba0285ff48538f4eb2c2de7e836f313bb8832fd8d86f71d62"}, + {file = "hypothesis-6.170.14-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:744c587791b2c5e38d1da2eaca617b1a82073832dfcf42975ddb03e93f187919"}, + {file = "hypothesis-6.170.14-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d0daded6c2f3e05f6bd989059bb798e570722802e05eb3543c6f5d7a1e7e6500"}, + {file = "hypothesis-6.170.14-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:986eaec8ca5a22710b1086b88734035c7d7ffcdb4ce02341b9f7069cc1bdfc5e"}, + {file = "hypothesis-6.170.14-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44c481115770bb3df5014b0369ea1aaf7319b81b3346a744a9265fc933190c3d"}, + {file = "hypothesis-6.170.14-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:261ae20a5573cf59e5dbf931c7fb9abcb012695adccb61fdab108cbba1772ee5"}, + {file = "hypothesis-6.170.14-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:833f7e6d93c03f3c00583a126e61f631d6b91a84e157a30da2d1c786ce5658b7"}, + {file = "hypothesis-6.170.14-cp314-cp314t-win_amd64.whl", hash = "sha256:224cad0e7ba8bfc6ef708652a3cc2148161e3157d191eb77ecc85dc5d16afb70"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:e76877e7c7a5cf8c4fbf98280dc6582f5d6450a73494490e8b48b7d03fb5e12f"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:5a613867bd2675101dda8fda380c3ce6be47842bed5640e1ba7b6b6fd0ffe8a3"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:a2249f2428ea94c47bccceb9fb8beada242c577dd4b3cd2edef5fb7160d1b71b"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:20df34cca1265c1683ccdcab5872cf0a023f40235a3092a96d4bd81e4fc57986"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4a7f8023efbec61296fd20edb35d638ec89ff761d3f9d3dc8128ac8f204652ec"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3faa2b45f30320fb57ce1c30f1bf9f08b1fd1cc1c606ae2a3e1efd0256fd7c77"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5be7a2239adde6c72001eb42cdb924d3c3856b1ee80342049961efdd6c72d064"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1a7aa883cd30c6c0c7259f34c1e3f37617c0d52fa6718e93c9926cc49b8047d6"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:79360d1dd671e602d2327e26fb9179645511adcfb9ec9d28c5d6bda313920aa2"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e4c45885e931a55c5097ebf0a9b65459878ad5e1c69c0448ae37e03fd764ddfc"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:a6d09601401c019366826effa98ae64393dd149b50d7d6ea202ad472784278a8"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:05e2a645b94fd43107a3360fe0aaf6db268199c80a43cf375fac5f1f9047745f"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:cb54946f70ae5008ea8cd58ce9a27b2126506732b53b83a6d117371d7c755362"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:b72805cf56012a3d96f4a1fbb869f178d448f242b4f64310956134a3c952113f"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:a17500680ec2c3cf006c12d3832a6c8daba8a18386f93b3d1828de07b07910aa"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-win32.whl", hash = "sha256:b1d9baaef909100fd1a6ba5457ecffa4a184e820c7f0bd3d4589f14870f5cade"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a725b854c5df088c5a579f1c323657258abe5d8e32e16cd07beb48960f31c070"}, + {file = "hypothesis-6.170.14-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:75f60ef38c010742339a9b2ec2a44cb0aed6c6c8f1455ad6a6de48f5216a72b2"}, + {file = "hypothesis-6.170.14-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd353f6368bfd860bd6fa713dae8037bfefeae12e4dde4292661d0e351b5b266"}, + {file = "hypothesis-6.170.14-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b225ad942600271fcf8d810856fa37e3fb21860ccc752a2b8efa2fb0eca5a7a2"}, + {file = "hypothesis-6.170.14-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9eda91235f462e9b98e25295badae1cce6406aa587207fe0a23015601e1acf59"}, + {file = "hypothesis-6.170.14-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f44ac697830ea44ac319311a86e892df38c6706f3ea9f98b148222dca63b8852"}, + {file = "hypothesis-6.170.14-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7226b109f964525bf8830f4543925d5dcf30f488ccd2f55ff9fd50a33629d7b7"}, + {file = "hypothesis-6.170.14-pp312-pypy312_pp80-macosx_10_12_x86_64.whl", hash = "sha256:c7b5dee8b87f8a78c1786eb1f96a6c0c91fcc2e10b2e9d876dcbc8e3f65dffc3"}, + {file = "hypothesis-6.170.14-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:e99d1bc7d8e8fc17cb67a38e003cbf4a17f41f7851c1f205efd35b6f3ba93da5"}, + {file = "hypothesis-6.170.14-pp312-pypy312_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d66dab3b79eda78964665ab4f9d7a269137c14cd6ec7ea4fb27002a8786452b9"}, + {file = "hypothesis-6.170.14-pp312-pypy312_pp80-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:88d829c29907e7d86f8b1d33041bb73814062a3681ec51dcb231f64874f88b14"}, + {file = "hypothesis-6.170.14-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:90d4e85f22c3154a59df851bb2c59c89c129fba3155125d98ebad80bf69aad26"}, + {file = "hypothesis-6.170.14.tar.gz", hash = "sha256:8272913eb34c4399a88cf8a9ecdb351c9ef8f621c061f2ac323362d4a66b755d"}, ] [[package]] name = "idna" -version = "3.10" -requires_python = ">=3.6" +version = "3.20" +requires_python = ">=3.9" summary = "Internationalized Domain Names in Applications (IDNA)" groups = ["default"] files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, + {file = "idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c"}, + {file = "idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44"}, ] [[package]] name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" +version = "2.3.1" +requires_python = ">=3.10" summary = "brain-dead simple config-ini parsing" groups = ["dev"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7"}, + {file = "iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960"}, ] [[package]] name = "jinja2" -version = "3.1.5" +version = "3.1.6" requires_python = ">=3.7" summary = "A very fast and expressive template engine." groups = ["default"] @@ -327,92 +584,271 @@ dependencies = [ "MarkupSafe>=2.0", ] files = [ - {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[[package]] +name = "librt" +version = "0.16.0" +requires_python = ">=3.9" +summary = "Mypyc runtime library" +groups = ["dev"] +marker = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18"}, + {file = "librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602"}, + {file = "librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96"}, + {file = "librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7"}, + {file = "librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d"}, + {file = "librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678"}, + {file = "librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742"}, + {file = "librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07"}, + {file = "librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b"}, + {file = "librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a"}, + {file = "librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940"}, + {file = "librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc"}, + {file = "librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a"}, + {file = "librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6"}, + {file = "librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e"}, + {file = "librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d"}, + {file = "librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4"}, + {file = "librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92"}, + {file = "librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91"}, + {file = "librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1"}, + {file = "librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667"}, + {file = "librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152"}, + {file = "librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839"}, + {file = "librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7"}, + {file = "librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41"}, + {file = "librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99"}, + {file = "librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc"}, + {file = "librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1"}, + {file = "librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7"}, + {file = "librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d"}, + {file = "librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b"}, + {file = "librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff"}, + {file = "librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d"}, + {file = "librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42"}, + {file = "librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a"}, + {file = "librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3"}, + {file = "librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971"}, + {file = "librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560"}, + {file = "librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b"}, + {file = "librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba"}, + {file = "librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9"}, + {file = "librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc"}, + {file = "librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866"}, + {file = "librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc"}, ] [[package]] name = "markdown-it-py" -version = "3.0.0" -requires_python = ">=3.8" +version = "4.2.0" +requires_python = ">=3.10" summary = "Python port of markdown-it. Markdown parsing, done right!" groups = ["default"] dependencies = [ "mdurl~=0.1", ] files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, ] [[package]] name = "markupsafe" -version = "3.0.2" +version = "3.0.4" requires_python = ">=3.9" summary = "Safely add untrusted strings to HTML/XML markup." groups = ["default"] files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, + {file = "markupsafe-3.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346"}, + {file = "markupsafe-3.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7"}, + {file = "markupsafe-3.0.4-cp311-cp311-win32.whl", hash = "sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5"}, + {file = "markupsafe-3.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3"}, + {file = "markupsafe-3.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e"}, + {file = "markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6"}, + {file = "markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e"}, + {file = "markupsafe-3.0.4-cp312-cp312-win32.whl", hash = "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237"}, + {file = "markupsafe-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7"}, + {file = "markupsafe-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9"}, + {file = "markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1"}, + {file = "markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e"}, + {file = "markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248"}, + {file = "markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17"}, + {file = "markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0"}, + {file = "markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5"}, + {file = "markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc"}, + {file = "markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed"}, + {file = "markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6"}, + {file = "markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634"}, + {file = "markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4"}, + {file = "markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c"}, + {file = "markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe"}, + {file = "markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a"}, + {file = "markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2"}, + {file = "markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c"}, + {file = "markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65"}, + {file = "markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429"}, + {file = "markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8"}, + {file = "markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741"}, + {file = "markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b"}, + {file = "markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214"}, + {file = "markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67"}, + {file = "markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad"}, + {file = "markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba"}, + {file = "markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6"}, ] [[package]] @@ -428,402 +864,383 @@ files = [ [[package]] name = "mypy" -version = "1.14.0" -requires_python = ">=3.8" +version = "2.4.1" +requires_python = ">=3.10" summary = "Optional static typing for Python" groups = ["dev"] dependencies = [ + "ast-serialize<1.0.0,>=0.12.1", + "librt>=0.16.0; platform_python_implementation != \"PyPy\"", "mypy-extensions>=1.0.0", + "pathspec>=1.0.0", "tomli>=1.1.0; python_version < \"3.11\"", - "typing-extensions>=4.6.0", -] -files = [ - {file = "mypy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e971c1c667007f9f2b397ffa80fa8e1e0adccff336e5e77e74cb5f22868bee87"}, - {file = "mypy-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e86aaeaa3221a278c66d3d673b297232947d873773d61ca3ee0e28b2ff027179"}, - {file = "mypy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1628c5c3ce823d296e41e2984ff88c5861499041cb416a8809615d0c1f41740e"}, - {file = "mypy-1.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fadb29b77fc14a0dd81304ed73c828c3e5cde0016c7e668a86a3e0dfc9f3af3"}, - {file = "mypy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:3fa76988dc760da377c1e5069200a50d9eaaccf34f4ea18428a3337034ab5a44"}, - {file = "mypy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e73c8a154eed31db3445fe28f63ad2d97b674b911c00191416cf7f6459fd49a"}, - {file = "mypy-1.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:273e70fcb2e38c5405a188425aa60b984ffdcef65d6c746ea5813024b68c73dc"}, - {file = "mypy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1daca283d732943731a6a9f20fdbcaa927f160bc51602b1d4ef880a6fb252015"}, - {file = "mypy-1.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7e68047bedb04c1c25bba9901ea46ff60d5eaac2d71b1f2161f33107e2b368eb"}, - {file = "mypy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:7a52f26b9c9b1664a60d87675f3bae00b5c7f2806e0c2800545a32c325920bcc"}, - {file = "mypy-1.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d5326ab70a6db8e856d59ad4cb72741124950cbbf32e7b70e30166ba7bbf61dd"}, - {file = "mypy-1.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf4ec4980bec1e0e24e5075f449d014011527ae0055884c7e3abc6a99cd2c7f1"}, - {file = "mypy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:390dfb898239c25289495500f12fa73aa7f24a4c6d90ccdc165762462b998d63"}, - {file = "mypy-1.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e026d55ddcd76e29e87865c08cbe2d0104e2b3153a523c529de584759379d3d"}, - {file = "mypy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:585ed36031d0b3ee362e5107ef449a8b5dfd4e9c90ccbe36414ee405ee6b32ba"}, - {file = "mypy-1.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9f6f4c0b27401d14c483c622bc5105eff3911634d576bbdf6695b9a7c1ba741"}, - {file = "mypy-1.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b2280cedcb312c7a79f5001ae5325582d0d339bce684e4a529069d0e7ca1e7"}, - {file = "mypy-1.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:342de51c48bab326bfc77ce056ba08c076d82ce4f5a86621f972ed39970f94d8"}, - {file = "mypy-1.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00df23b42e533e02a6f0055e54de9a6ed491cd8b7ea738647364fd3a39ea7efc"}, - {file = "mypy-1.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e8c8387e5d9dff80e7daf961df357c80e694e942d9755f3ad77d69b0957b8e3f"}, - {file = "mypy-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14117b9da3305b39860d0aa34b8f1ff74d209a368829a584eb77524389a9c13e"}, - {file = "mypy-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af98c5a958f9c37404bd4eef2f920b94874507e146ed6ee559f185b8809c44cc"}, - {file = "mypy-1.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b343a1d3989547024377c2ba0dca9c74a2428ad6ed24283c213af8dbb0710b"}, - {file = "mypy-1.14.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cdb5563c1726c85fb201be383168f8c866032db95e1095600806625b3a648cb7"}, - {file = "mypy-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:74e925649c1ee0a79aa7448baf2668d81cc287dc5782cff6a04ee93f40fb8d3f"}, - {file = "mypy-1.14.0-py3-none-any.whl", hash = "sha256:2238d7f93fc4027ed1efc944507683df3ba406445a2b6c96e79666a045aadfab"}, - {file = "mypy-1.14.0.tar.gz", hash = "sha256:822dbd184d4a9804df5a7d5335a68cf7662930e70b8c1bc976645d1509f9a9d6"}, + "typing-extensions>=4.14.0; python_version >= \"3.15\"", + "typing-extensions>=4.6.0; python_version < \"3.15\"", +] +files = [ + {file = "mypy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:79a1e79199e980fb41f219cb3d4746d25c64b0b623840d3ed57df1cdc13a6502"}, + {file = "mypy-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e18d0da3898b3bcd16192f15a206e7b8b8dd0f4a0b3e51aa5bd5c2afe749e2c"}, + {file = "mypy-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69a15fc2e75858d6b15e91ed3a0dcff1a533265183b64734138791fe0f49aa5"}, + {file = "mypy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70971cc0bcb1d3f03d2eb68fa430a6481ccc03558dc001d9534d7e5fa274c392"}, + {file = "mypy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f7225951a277dd0d6add900731d487174b7d24ac133c09d670ed63b4d8cb90d"}, + {file = "mypy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:be4c03f33c55500191df2ed4a9b5e7edbdd1fff067fe361a5a9b1b5d3262da6e"}, + {file = "mypy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d63737426a048a27705a4eedb828669f4bc0873612a1b46b0b9fe892b2ebaaaf"}, + {file = "mypy-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7344869cad05f073b8cae2826774fa0161e3eb2f5c685b76813574008d77d750"}, + {file = "mypy-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a20ff6cd2af7a670c64f089bfa5fbcebe8ff3bf896c5e6ac7c9862c7960e4494"}, + {file = "mypy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ed1195502cca32be5ef756b1693477f4991db2f2e34eaa90cc4ad6bea4f9531"}, + {file = "mypy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c194c5d878130fca1ae662dfb1feb6274afbab0b40032c80147377e0ea803d5"}, + {file = "mypy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:71eda06e5bae51976ce869fda429159c9cec678bd02f25bb197f0265c8f991be"}, + {file = "mypy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f1613bfe6726169a5680ec3dad5fe1f6d96285c12aded51ad432eadedd39d45"}, + {file = "mypy-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:680ba5d9f0e3fe0c746b3d14df15f2a718e4bc308a1145616a1ea691ce7c354b"}, + {file = "mypy-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb70621535c22fd4b1a0450d37ed1d6c5eb39656a1cebfb189084c12de54ba7"}, + {file = "mypy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4af5ad558033bf71a44a1be149ed61633522824f84c1b6bc2445d3fe2d2a5296"}, + {file = "mypy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:acdd82cb3a2eb10720e87706a128d75785d005a8c47e36a4973221524f41fc56"}, + {file = "mypy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:133886ef1a7a8cf673df55d865e41580c6c46aa9b8e31e1325dfe315c038f745"}, + {file = "mypy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c45485061ba08f700acc12cfc8b177e6997e6bc353605babfc0abb081e079456"}, + {file = "mypy-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727890a32f1858c09549eb55a8b234fe990d460450b28f03e8213ef925f1247c"}, + {file = "mypy-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:beb254fec368c4e7f7f82a5f310c6470b893ec88a90b66426da971c6826b0453"}, + {file = "mypy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ec416d5d2ae689e681f5727e328723f18d8b5dc8c903a146b7851dc5727aad8a"}, + {file = "mypy-2.4.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:0e95ad0bec03ced9a73b42514208abd24c789e67f0e1eba476e276dd7a2dea3a"}, + {file = "mypy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:51f742d4154824837ca6e92aa81f00eec222700a3c337bd258e5562a3880d75a"}, + {file = "mypy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:f44dd4c5ab22df21206dd926c98381ebdbf5bfd696df37dd38ded2d23143fcb6"}, + {file = "mypy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2875280687e64740e6ddcef43bd4c5852ad45ff34549740f6aea3f0373870b93"}, + {file = "mypy-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea0714d2bccaf6d010c970e1d3ba2c9c2add79d2030aad2ef03167d788948276"}, + {file = "mypy-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c6ff0e7ea546943001a91639c9f0c64b60d239268fa5d29e4a6c87f82c2373"}, + {file = "mypy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a5b5da107dc2c93fb0a22f7a45dbaea5dc131afc7056b5468d78e819f9a138e6"}, + {file = "mypy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c3a4d0c47053cc90035dd3a2c321112b593ad42935d0cbfd747288e1564fa8ca"}, + {file = "mypy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0c5bf267645351887b72903fd2f5fbddcf85234e0e6c4ad94e2fcfb4f0c3f971"}, + {file = "mypy-2.4.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:db599d03e7d60d15f2e0a11665c18bc52a88699fcab990fdf8cecd5be8ac801c"}, + {file = "mypy-2.4.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6c7210e40dd423af7d65cbe428b5906d11dc60659337bdae4e5fdf02ce32ed7"}, + {file = "mypy-2.4.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2d4a51db92059eecc2c33b2a5194b28585ebfd917292c015fe87a77dbff6cd2"}, + {file = "mypy-2.4.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:01b7718552d017f88d1151f3cc9d6556d778a90bd58060a2c3d87d070f9099fb"}, + {file = "mypy-2.4.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:3467b1d13000812ab85dc2a3834b9379ec77798ca1bcabe434e23fb21fead692"}, + {file = "mypy-2.4.1-cp315-cp315-win_amd64.whl", hash = "sha256:080cf64a60b90a73435542feecf4171904cc5710658165b40c6869115d4aa100"}, + {file = "mypy-2.4.1-cp315-cp315-win_arm64.whl", hash = "sha256:f9118a3f50de264b614861bb11814833a742138ac3c8e2e49a860b6977fb2704"}, + {file = "mypy-2.4.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9d7dafd56179d296651df9e1844b91c9517c379398f017a2a435d077a28943c1"}, + {file = "mypy-2.4.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d923cec43316f0e6736b8298fd00abeaaf348a091c20e7296b3aebceecb63eca"}, + {file = "mypy-2.4.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6e081b466e88d4343adffe11aff0f02df34b5b9070b4b700053a0b7294c0be1"}, + {file = "mypy-2.4.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:70196470ac300f444a302a738541101893c2c57804078b33690286a7899149e9"}, + {file = "mypy-2.4.1-cp315-cp315t-win_amd64.whl", hash = "sha256:de95648e0a6d46fa9929b956ed2c87c5dc9933d4d59945e6390e9d753c74e7c1"}, + {file = "mypy-2.4.1-cp315-cp315t-win_arm64.whl", hash = "sha256:ef8d5be90032261197ac388d945d8ff4f476c95880798a1911760af64ddfe71f"}, + {file = "mypy-2.4.1-py3-none-any.whl", hash = "sha256:1784648d6ec8c2b5465e3f26241b845a0b3cb4e56c993c64abe16989e7698999"}, + {file = "mypy-2.4.1.tar.gz", hash = "sha256:63b590965691e540a335fa74dadf31a7069142f6b72e234689bbd244edf55623"}, ] [[package]] name = "mypy-extensions" -version = "1.0.0" -requires_python = ">=3.5" +version = "1.1.0" +requires_python = ">=3.8" summary = "Type system extensions for programs checked with the mypy type checker." groups = ["dev"] files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, + {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 = "24.2" -requires_python = ">=3.8" +version = "26.3" +requires_python = ">=3.9" summary = "Core utilities for Python packages" groups = ["dev"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +requires_python = ">=3.9" +summary = "Utility library for gitignore style pattern matching of file paths." +groups = ["dev"] +files = [ + {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, + {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, ] [[package]] name = "pluggy" -version = "1.5.0" -requires_python = ">=3.8" +version = "1.7.0" +requires_python = ">=3.10" summary = "plugin and hook calling mechanisms for python" groups = ["dev"] files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec"}, + {file = "pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8"}, ] [[package]] name = "pydantic" -version = "2.10.4" -requires_python = ">=3.8" +version = "2.14.2" +requires_python = ">=3.10" summary = "Data validation using Python type hints" groups = ["default"] dependencies = [ "annotated-types>=0.6.0", - "pydantic-core==2.27.2", - "typing-extensions>=4.12.2", + "pydantic-core==2.50.2", + "typing-extensions>=4.16.0", + "typing-inspection>=0.4.4", ] files = [ - {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, - {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, + {file = "pydantic-2.14.2-py3-none-any.whl", hash = "sha256:7efbf7507b937931cd32a5755acb00ea1cf5ccb1d4e8067a5850d5069e382118"}, + {file = "pydantic-2.14.2.tar.gz", hash = "sha256:fb7b2db6cefe8414dd612edeede49c4c433acb0432b0f92b95024e45f66c505d"}, ] [[package]] name = "pydantic-core" -version = "2.27.2" -requires_python = ">=3.8" +version = "2.50.2" +requires_python = ">=3.10" summary = "Core functionality for Pydantic validation and serialization" groups = ["default"] dependencies = [ - "typing-extensions!=4.7.0,>=4.6.0", -] -files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, + "typing-extensions>=4.16.0", +] +files = [ + {file = "pydantic_core-2.50.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2115d7ca6d58554af8a14da3dd32d361a4fe999fae52f115a6155679b5b00e72"}, + {file = "pydantic_core-2.50.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2dd65864aad38dbcc57c3d0e2c978d4286e063378137d8fc0c9fcd558943285"}, + {file = "pydantic_core-2.50.2-cp311-cp311-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:26b778d4ebb75f181d36220ff0f54a513fc46f053d9b09ce79993c2f00bc45cc"}, + {file = "pydantic_core-2.50.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e80c47314c7233e7105a7efcfffd89d56e479e558189d1959eeeaf2639a27680"}, + {file = "pydantic_core-2.50.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:3bfe12243b3ded517c6734d91f127489e2f51bf95cb533b53130fe360333db80"}, + {file = "pydantic_core-2.50.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:01eb0282d0b3a065d55956d1be9ce398c46db3dd6d277a6cb2358c8aa8c53179"}, + {file = "pydantic_core-2.50.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:196b87b48b2695426920e8470d20f9e99f019ebd86f15808aa018b68086840cd"}, + {file = "pydantic_core-2.50.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86e7abd24a67f61a4bfe8df059b67a2bdd8bf33705178e98463195d140bd3780"}, + {file = "pydantic_core-2.50.2-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:aff31602e8518594e6f6bc670cdb27e37cb7dbeb2d5d0b76a7707b70fe951ddf"}, + {file = "pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a88851e8e332588cb8cced8b5318d1163c7d0bb87830aa8dcafbc4ae9602b73c"}, + {file = "pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3b1bb3950e0794a795c9344d818d5b476189b0c9c9755e8eb8fde87c3acee17a"}, + {file = "pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:928a44ad1e21a7e923eb924d601605f0694cd187d2ce5d40bf7ad76650f4627d"}, + {file = "pydantic_core-2.50.2-cp311-cp311-win32.whl", hash = "sha256:9a87744e683d9a13f60b7fa5242c17f8d283ffa94ad727cb19063b48c3d34960"}, + {file = "pydantic_core-2.50.2-cp311-cp311-win_amd64.whl", hash = "sha256:b958c217c6e56bef261f516e4caf5d490b079ad6a428752afada5a764cca6c71"}, + {file = "pydantic_core-2.50.2-cp311-cp311-win_arm64.whl", hash = "sha256:b0e6a2152706d84f7da8b9d34dcecc7e9bde678af32c40bd5b405f427ae0fbdb"}, + {file = "pydantic_core-2.50.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e3d824b6152e1c0fd58dc6266f2de470eb7b2286135294f5f7cd26a0f0693738"}, + {file = "pydantic_core-2.50.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:596921489390a002d6a5e303384a0f1f98384f8bb550fbf38a1aafa6093da0f9"}, + {file = "pydantic_core-2.50.2-cp312-cp312-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:36d8b9725684ca3bf658b90a101d335fc8cae405bb22196b7b5ef41344913756"}, + {file = "pydantic_core-2.50.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:43a8fd56f3c681f3cda2d5c842ad28e88241df742dfcbe8fd1dd437a3056e1f6"}, + {file = "pydantic_core-2.50.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:6f5a432c6c1ef18a1a02a04b3616b8d3f2f88b13faa46caae1ac6559ab202234"}, + {file = "pydantic_core-2.50.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d3cd8be71c4da3c9b42cc92f17e5e3036bdca98bf18555a0e072c3fe87abbb4"}, + {file = "pydantic_core-2.50.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a438cb8a3f87f2c49e82e43fb19511d4d5043f53e0787dc4df937ccb904373a"}, + {file = "pydantic_core-2.50.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9667637665acba166a7dbbc786f14c50512d3a01f7199e0e1a13b929e92d7c7a"}, + {file = "pydantic_core-2.50.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:b3e681208e2d39fde13fbea449ef2736a85ec1ea2afa41e9193a8c536e71dafe"}, + {file = "pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6eb6ff07964f39e33c6f57fefe939514e76b8c0d4a694e5663e2e04e80cc1798"}, + {file = "pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:4eb54c25e3463d73e1e4ed439ffc45d1d7e590df40bc7bb06f11a906afd996c3"}, + {file = "pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:91238117cd296352f54f24117ff5a227bdea428865dba1244aa0cdd1424c0bc2"}, + {file = "pydantic_core-2.50.2-cp312-cp312-win32.whl", hash = "sha256:98722314e12a8e4fa346e0afd3fff2098b5732875ff45592b450a7b589b43c4f"}, + {file = "pydantic_core-2.50.2-cp312-cp312-win_amd64.whl", hash = "sha256:b381574614245f18fdb2d6d1aa8f149e225869c47b5ee7f81812de8977817314"}, + {file = "pydantic_core-2.50.2-cp312-cp312-win_arm64.whl", hash = "sha256:f07c965a8aa6d62145f122641f292129984f39a3e3697585b19298a1488287db"}, + {file = "pydantic_core-2.50.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:93eb41d626d9a2749a678823c70bba8c25033cef474ae8001fd7621adbbd8437"}, + {file = "pydantic_core-2.50.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c539d452cba22f250103c7d1c00cbe9038eb705e657476e701c43374efd9f2e"}, + {file = "pydantic_core-2.50.2-cp313-cp313-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:53bd25e8ab54c02bb4c55d4848c1d36f75f3b9a7356e83179fe27794d9e9e6c4"}, + {file = "pydantic_core-2.50.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f142cd663d1148ed32993ed9a58e5c67ea312f446a01a6c13a956945aafc9f23"}, + {file = "pydantic_core-2.50.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:126f158474346f457c0759c29c1a6334acd5f7f99dd272bb27d19314f3d81b4d"}, + {file = "pydantic_core-2.50.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0b94a515f44f1ebcb25395bcdba506c4cdaf6d3fcb0466b9658521b41e5ff057"}, + {file = "pydantic_core-2.50.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a55c1163e829a08707b4a92c8ee02a5a3a3847ced74aa4da94e3e975ffe1ec63"}, + {file = "pydantic_core-2.50.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31a1fb3ffbd1195b24490f671d12623dd4c9bfa56d6af0a299fa43b516134f5b"}, + {file = "pydantic_core-2.50.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:83d6f42ae6eba44548a1a3d4f900a5b1500fdd14f09f04754a8800ab0d544432"}, + {file = "pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9fcde11462e0faab4c2a2ae8cd4516f7dd8239eec583bd88c5524d8064ad2d0c"}, + {file = "pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:848984e231190ae49bc8b5229e64d2a6fba4231dc6046e1b6b28ddc060bc7601"}, + {file = "pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8772c2c0091e6fc2d0499151793d31bf811826986b0727052935a95104845544"}, + {file = "pydantic_core-2.50.2-cp313-cp313-win32.whl", hash = "sha256:bd6ecae08c0d871cf93b6f0d4ee76b06c65be44ea22311981a022b2f1948cf9d"}, + {file = "pydantic_core-2.50.2-cp313-cp313-win_amd64.whl", hash = "sha256:e5c801bdf3cc92274b8fc2c7023c8751e5ae81260c009ac558ff55ce712a2d45"}, + {file = "pydantic_core-2.50.2-cp313-cp313-win_arm64.whl", hash = "sha256:fd9aa71a800c87c0f7ab53fa38009169a23a82c685989626ca74a4bd483e1b67"}, + {file = "pydantic_core-2.50.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:efad0bdc67d3b119c3bab6b94d341a27333c2bb89ed75dfa182a4a06cd0ea329"}, + {file = "pydantic_core-2.50.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ffb4dce3ecb2cd563aa30f9eb5088a1cf1f4f688b90cbb374fa96e49fc50b02"}, + {file = "pydantic_core-2.50.2-cp314-cp314-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:150f6e101f575c21a4ecfed1be1c05bb89a0ad49cb9c846ed377583bd71b32c2"}, + {file = "pydantic_core-2.50.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1db9cab6197e797824f0479dd2ae5fd2e272aa61c61b7faa5a6e450ca7a0d3b7"}, + {file = "pydantic_core-2.50.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1b9f652e17a13bbc09b45f74c3c326cca72059ce4dcdef687fcb04389db413a9"}, + {file = "pydantic_core-2.50.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:823b222c4fc861a82c650998a6117ecbd150cc71bf8b78f9a31bf3f48c456108"}, + {file = "pydantic_core-2.50.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:783abdfac21247d33bec40aa683d26bba30d61655c40de38b2decc4797aabf92"}, + {file = "pydantic_core-2.50.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b29b7c5adf4b2be7c941f7f76432aa3269cd5052ff8610376b8ece24d7b02c51"}, + {file = "pydantic_core-2.50.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4ac996e42a93d80122a23edce44e02c6b9c4913f5c6198a077da31475341a39a"}, + {file = "pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:99e1d482138a71902415ba375bb4627679a5fefd3bd07803c3a8f37632f4981d"}, + {file = "pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:a53d5f11cd6d15569ba3e53f3498077f0d5e633f6ec604d2ea613e895017ebee"}, + {file = "pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23385c4626f8b5a7792aa381ea573d9432cfbf4c21375a0a5eed2a4fb64e0e58"}, + {file = "pydantic_core-2.50.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9a8a62281fd9c6a373b3fd3d473685e6838d43341eecde5f47729748e2c11ee3"}, + {file = "pydantic_core-2.50.2-cp314-cp314-win32.whl", hash = "sha256:18f008ef58cd30782f81b6e0119e20b7e884edcefbc2d370162ebe033f6e5c99"}, + {file = "pydantic_core-2.50.2-cp314-cp314-win_amd64.whl", hash = "sha256:b62c89545d21d930c333208fd548e7b4f262228ba3a322fd75c987a75ab11f31"}, + {file = "pydantic_core-2.50.2-cp314-cp314-win_arm64.whl", hash = "sha256:6e731bf5a5e8e25398544d7709e39068ec4230b0177e4db7fa955692fd415ba3"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:33c6800ab9563adaa0d7893e9cd8f5b972ef1e6a23df70498e05c24f3fcec5e1"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f81df206b937d0dca5efb38c25fc99037d82dbcd3740e34c3f81997d2322ac1c"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21ea3b00f1e87c66f3f9e92f9f6fec20495ac0e60d7d2e68ba1fc49739d7cf40"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9eff570402175c7d5fff8497d5a9c05580664a79bb66c07b8f205d17f69e580b"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:240fec32a74af0979f7253532859b3fd209ad5eea822416bb010929dba445c3d"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92acf02c770b9d5c4e9a3e8d64e9d95ae26456a86ebb7b7cf64a97f899e5871b"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1ac5b99bc7b0a68ec64f5f75a6828a7bab5535c8df0cdc07901deb50e0815910"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:713d7c33c0e231e3786eb0ec95885814dd65d5f3a09754399b6acbf5e9365f11"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:61e13100b30d8d25fc0d1f0297014b32941bf0975a3584bd08f3adebb68d1ead"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:523fe6574aea50f456cbc301c1cdddcbf237939f7657f9af99b4f0f71942b59c"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:6c25b8826be85714b27dafe908d3461e1b09e3e31c0279a7029040c832c781ef"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a59db03b80a156a6fbeae41096866d1cb89cdf31031ba44c768a804538880636"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-win32.whl", hash = "sha256:0d090ae76d5b09c003600c7779b3adcf3d1b49268f616c5c51d844e54c8e318d"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9b01a092955c1c7530196ad526755cff4f2d11f37ca9a6fc9ebe37ca49a48977"}, + {file = "pydantic_core-2.50.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4ea01f30e119f1ec7a2034e85c40f491d469896aef02cd6436fd9292fe1be3fb"}, + {file = "pydantic_core-2.50.2-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:a3317630e2791f56071b82f89bb71624146125e4311e6643f708c6ba744b5ac5"}, + {file = "pydantic_core-2.50.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f73930dacad5e611fe019e18df6f53c30629b1843d038d303b8dec86a32493bd"}, + {file = "pydantic_core-2.50.2-cp315-cp315-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:e2e6240a0a5ad847017b3d65187877fdb7a11d5a58d43c30736f9f523478913b"}, + {file = "pydantic_core-2.50.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fdf5b52ed22f1404c2433d0b46df2d86d9befb742d0b9b4042a2325c43cdb97"}, + {file = "pydantic_core-2.50.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4e9392ddf67fe97ab9889f796d468c9908a8754b8b6c7520eb03a65012b9a1e7"}, + {file = "pydantic_core-2.50.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:48e944091b7bff233920bed8a60b0ed6c96c73d3c5836bf51c4b9a4bfb58e4ca"}, + {file = "pydantic_core-2.50.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:91c92856b846aa690ea6239ffd6c5003c5e3e296a69095570bf4efee5bfa0bdc"}, + {file = "pydantic_core-2.50.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5299296ff5cafcb69851fd652de8f9268a0bb036bcbedbfcaa94cf743baf3986"}, + {file = "pydantic_core-2.50.2-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d3d049b557875f9d15230eec8a731749a7a873d8ed6a051903b05bdf46e0e65f"}, + {file = "pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b0b25d647036972c2951059046cb498f72b7af5a2def27bf8e5f44c3067ab7dd"}, + {file = "pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:8835566b3e94026da3ce2249f9ee35c9d2ebe4cd131f6a39bfc82708d3010a46"}, + {file = "pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:3fbcd402e4b607d4b8de5d4591215f31f042af03f9d09e58f693a6221e32833f"}, + {file = "pydantic_core-2.50.2-cp315-cp315-win32.whl", hash = "sha256:8431ecb131489cbdaf6f51f7ade47475f73aff4f1912552f75ba19a6cfe233a8"}, + {file = "pydantic_core-2.50.2-cp315-cp315-win_amd64.whl", hash = "sha256:04cf672960b8c4fc28bb159c8cdd101fbd311c520237a5c0360448b851adfb41"}, + {file = "pydantic_core-2.50.2-cp315-cp315-win_arm64.whl", hash = "sha256:36917e29c0b815d6b940bc91560fcf453b5b85d55c579796373f0ea11d1c111a"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:bd1fe69056a19c377a6c6de2c1240fb8676ba0512a969965efb486510c5e0f1f"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9b2383933b1dd7a8c93e02f99ebfd305f1286057172483b8e6898bed3fef69d3"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:a1b4d60bad57d6e0e7cd30cc4d1dc646d258dd6dfb7935ce76c7dd72dd1e98b0"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:93bedc68c4138088ea3c042189c01d2945f530f0dcc36e1ea02df8418bd4f1cd"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:d684ff87d00de2f6abcbbfa9b0fdce8b513dd799a900809dcfa7eb62ad69832a"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bb6d49f00e2babd40af4889d6b9f4bf50879f522dd4e2e22cb705cac5e0c5e8e"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6d47b2d0e00e030e0ebdb08115a37da7294e7a6d196d134a77ed7ee6c142284b"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95838be125133fa314753f8cc5168f7c41e3a9dd5eef5383dbbbeed511bd4853"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:2d4a043c820363359e40ac96270e1915e5376e116be1f59cee0701775de3e376"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:57bccd3e1128736c792094da1feac6da46ec3feb1957ef649a9b3e4f8de6fa04"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:24120c644322a35b5eb765416ac8442214c45689e255f5fc6e761e7e7c743c8b"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:b1adf2f37521b578c438f6ec61d1c1d0c20b076863ddd5d8b6f3ab658857db3f"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-win32.whl", hash = "sha256:d79aaa468193c1bed6b0560d3c5c9f9e369e3efb0b128b38a2537e2d51d42773"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-win_amd64.whl", hash = "sha256:cc488daac19e6f5292180fac5e0ef7efffabeef4e096af9a17a75a7d3d6781f6"}, + {file = "pydantic_core-2.50.2-cp315-cp315t-win_arm64.whl", hash = "sha256:91315aea7adb51821d4a3378ede953bfdd76e3bf71551de4a82d7638cef7dc02"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b583ddeee6a6bb5bbc04462167c68034913221139d52d9f155ca6d1637a2fdcf"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5ab5738c39b25b5dc5a721270e2269a2f5e86e614375e6c07ee6caab8cbc0f2f"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21a902e546032c82b7275552345a2342d862e0541aea7226c91067c396e68b3f"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92ada606285dab054e40848a951f583c537cde1e54701ea1305ec0096877012d"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c1b65a42afc73b39e6ae908ec7b9c3b9898676a7b15ce4c66d4965f7071007c8"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c324a7e0c0012e3974053f81a270d8f1aa23227571f4ddd3254c911bb356459b"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:cb8d2d7b07cbe2d7488de7e53671f836c4f4a8032dcda079df33dc1b7e2397c0"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7c6e1bd6cadae13958e52897ef020e85c9916a95917149fca463bb0c0421a7c6"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:f078e276848bc23d1f92f46fc88d5b9cbb824b9acc60d9e07116620e502e16a9"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:3435c443da7510a6de1a30c68beae338852a11f0440cc8320ea756a162f38dbe"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp80-manylinux_2_28_x86_64.whl", hash = "sha256:b5675a12a7b67063fe3ca187e735b0463cf13fabc10166ae4d4b2f775fd0632d"}, + {file = "pydantic_core-2.50.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:f5ba7ef10ce82737e173fa95cafa1c8210918f7e653715b2c7a7a31877a74848"}, + {file = "pydantic_core-2.50.2.tar.gz", hash = "sha256:1b0e8dc3932cf4e9ed5727570325cacc716b9409ffc794a221c0ccc72e8eb775"}, ] [[package]] name = "pygments" -version = "2.18.0" -requires_python = ">=3.8" +version = "2.21.0" +requires_python = ">=3.9" summary = "Pygments is a syntax highlighting package written in Python." -groups = ["default"] +groups = ["default", "dev"] files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, ] [[package]] name = "pytest" -version = "8.3.4" -requires_python = ">=3.8" +version = "9.2.0" +requires_python = ">=3.10" summary = "pytest: simple powerful testing with Python" groups = ["dev"] dependencies = [ - "colorama; sys_platform == \"win32\"", - "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", - "iniconfig", - "packaging", - "pluggy<2,>=1.5", - "tomli>=1; python_version < \"3.11\"", + "colorama>=0.4; sys_platform == \"win32\"", + "exceptiongroup>=1; python_version < \"3.11\"", + "iniconfig>=2", + "packaging>=24", + "pluggy<2,>=1.6", + "pygments>=2.15", + "tomli>=2; python_version < \"3.11\"", ] files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, + {file = "pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588"}, + {file = "pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b"}, ] [[package]] name = "pytest-cov" -version = "6.0.0" +version = "7.1.0" requires_python = ">=3.9" summary = "Pytest plugin for measuring coverage." groups = ["dev"] dependencies = [ - "coverage[toml]>=7.5", - "pytest>=4.6", + "coverage[toml]>=7.10.6", + "pluggy>=1.2", + "pytest>=7", ] files = [ - {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, - {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, + {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, + {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, ] [[package]] name = "pytest-mock" -version = "3.14.0" -requires_python = ">=3.8" +version = "3.17.0" +requires_python = ">=3.10" summary = "Thin-wrapper around the mock package for easier use with pytest" groups = ["dev"] dependencies = [ "pytest>=6.2.5", ] files = [ - {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, - {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -groups = ["default"] -dependencies = [ - "six>=1.5", -] -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"}, + {file = "pytest_mock-3.17.0-py3-none-any.whl", hash = "sha256:1b82aa4b37137248bd687055e109723debeb07b489de1565297dc9589c5f2d6b"}, + {file = "pytest_mock-3.17.0.tar.gz", hash = "sha256:e36576e0985e01027e999891e7a966c67822d40c89ef1b4c8b6e22737e3590b4"}, ] [[package]] name = "python-multipart" -version = "0.0.20" -requires_python = ">=3.8" +version = "0.0.33" +requires_python = ">=3.11" summary = "A streaming multipart parser for Python" groups = ["dev"] files = [ - {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, - {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, + {file = "python_multipart-0.0.33-py3-none-any.whl", hash = "sha256:8f16c07b36add9da6b2ee87d3efcaf0a45a6de9c3d0a5dd94e9ccf7675a7bc45"}, + {file = "python_multipart-0.0.33.tar.gz", hash = "sha256:b8112424a6abd162ec08d390565dd113ad699f2fe6fef3c987b8bbcbfe9e390d"}, ] [[package]] name = "rich" -version = "13.9.4" -requires_python = ">=3.8.0" +version = "15.0.0" +requires_python = ">=3.9.0" summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" groups = ["default"] dependencies = [ "markdown-it-py>=2.2.0", "pygments<3.0.0,>=2.13.0", - "typing-extensions<5.0,>=4.0.0; python_version < \"3.11\"", ] files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, ] [[package]] name = "ruamel-yaml" -version = "0.18.6" -requires_python = ">=3.7" -summary = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -groups = ["default", "dev"] -dependencies = [ - "ruamel-yaml-clib>=0.2.7; platform_python_implementation == \"CPython\" and python_version < \"3.13\"", -] -files = [ - {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, - {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, -] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.12" +version = "0.19.1" requires_python = ">=3.9" -summary = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -groups = ["default", "dev"] -marker = "platform_python_implementation == \"CPython\" and python_version < \"3.13\"" -files = [ - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, - {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, -] - -[[package]] -name = "ruamel-yaml-string" -version = "0.1.1" -requires_python = ">=3" -summary = "add dump_to_string/dumps method that returns YAML document as string" -groups = ["dev"] -dependencies = [ - "ruamel-yaml>=0.17.17", -] +summary = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +groups = ["default"] files = [ - {file = "ruamel.yaml.string-0.1.1-py3-none-any.whl", hash = "sha256:eb146bcb42b116216638034a434e9cf3ae2a5d3933aa37183a9854b5f3ff42de"}, - {file = "ruamel.yaml.string-0.1.1.tar.gz", hash = "sha256:7a7aedcc055d45c004d38b756f58474ebefb106851f4ce56ce58415709784350"}, + {file = "ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93"}, + {file = "ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993"}, ] [[package]] name = "ruff" -version = "0.8.4" +version = "0.17.3" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." -groups = ["default"] +groups = ["default", "dev"] files = [ - {file = "ruff-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60"}, - {file = "ruff-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac"}, - {file = "ruff-0.8.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ddf5d654ac0d44389f6bf05cee4caeefc3132a64b58ea46738111d687352296"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e248b1f0fa2749edd3350a2a342b67b43a2627434c059a063418e3d375cfe643"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf197b98ed86e417412ee3b6c893f44c8864f816451441483253d5ff22c0e81e"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c41319b85faa3aadd4d30cb1cffdd9ac6b89704ff79f7664b853785b48eccdf3"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9f8402b7c4f96463f135e936d9ab77b65711fcd5d72e5d67597b543bbb43cf3f"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4e56b3baa9c23d324ead112a4fdf20db9a3f8f29eeabff1355114dd96014604"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:736272574e97157f7edbbb43b1d046125fce9e7d8d583d5d65d0c9bf2c15addf"}, - {file = "ruff-0.8.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fe710ab6061592521f902fca7ebcb9fabd27bc7c57c764298b1c1f15fff720"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:13e9ec6d6b55f6da412d59953d65d66e760d583dd3c1c72bf1f26435b5bfdbae"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:97d9aefef725348ad77d6db98b726cfdb075a40b936c7984088804dfd38268a7"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ab78e33325a6f5374e04c2ab924a3367d69a0da36f8c9cb6b894a62017506111"}, - {file = "ruff-0.8.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8ef06f66f4a05c3ddbc9121a8b0cecccd92c5bf3dd43b5472ffe40b8ca10f0f8"}, - {file = "ruff-0.8.4-py3-none-win32.whl", hash = "sha256:552fb6d861320958ca5e15f28b20a3d071aa83b93caee33a87b471f99a6c0835"}, - {file = "ruff-0.8.4-py3-none-win_amd64.whl", hash = "sha256:f21a1143776f8656d7f364bd264a9d60f01b7f52243fbe90e7670c0dfe0cf65d"}, - {file = "ruff-0.8.4-py3-none-win_arm64.whl", hash = "sha256:9183dd615d8df50defa8b1d9a074053891ba39025cf5ae88e8bcb52edcc4bf08"}, - {file = "ruff-0.8.4.tar.gz", hash = "sha256:0d5f89f254836799af1615798caa5f80b7f935d7a670fad66c5007928e57ace8"}, + {file = "ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4"}, + {file = "ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11"}, + {file = "ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5"}, + {file = "ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a"}, + {file = "ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb"}, + {file = "ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2"}, + {file = "ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe"}, + {file = "ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca"}, + {file = "ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24"}, + {file = "ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9"}, + {file = "ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9"}, ] [[package]] @@ -838,98 +1255,56 @@ files = [ ] [[package]] -name = "six" -version = "1.17.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Python 2 and 3 compatibility utilities" -groups = ["default"] -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 = "sniffio" -version = "1.3.1" -requires_python = ">=3.7" -summary = "Sniff out which async library your code is running under" -groups = ["default"] +name = "sortedcontainers" +version = "2.4.0" +summary = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +groups = ["dev"] files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] [[package]] name = "syrupy" -version = "4.8.0" -requires_python = ">=3.8.1" +version = "6.1.1" +requires_python = ">=3.10" summary = "Pytest Snapshot Test Utility" groups = ["dev"] dependencies = [ - "pytest<9.0.0,>=7.0.0", + "pytest>=8.0.0", ] files = [ - {file = "syrupy-4.8.0-py3-none-any.whl", hash = "sha256:544f4ec6306f4b1c460fdab48fd60b2c7fe54a6c0a8243aeea15f9ad9c638c3f"}, - {file = "syrupy-4.8.0.tar.gz", hash = "sha256:648f0e9303aaa8387c8365d7314784c09a6bab0a407455c6a01d6a4f5c6a8ede"}, + {file = "syrupy-6.1.1-py3-none-any.whl", hash = "sha256:6ac9ac6a42d70ae44156f5f157a9563b9bb8cc96dcf9364327f330463bff889a"}, + {file = "syrupy-6.1.1.tar.gz", hash = "sha256:7e584a8df4ede6dd3c1c44a77f9edc46186b6767a73dea6f0083044946839a59"}, ] [[package]] -name = "tomli" -version = "2.2.1" -requires_python = ">=3.8" -summary = "A lil' TOML parser" -groups = ["dev"] -marker = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +name = "truststore" +version = "0.10.5" +requires_python = ">=3.10" +summary = "Verify certificates using native system trust stores" +groups = ["default"] +marker = "sys_platform != \"emscripten\"" +files = [ + {file = "truststore-0.10.5-py3-none-any.whl", hash = "sha256:9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c"}, + {file = "truststore-0.10.5.tar.gz", hash = "sha256:30d36967ccaded5cbb38d602c433f53600036c79d502f4533a49b60a03bbefcd"}, ] [[package]] name = "typer" -version = "0.15.1" -requires_python = ">=3.7" +version = "0.27.3" +requires_python = ">=3.10" summary = "Typer, build great CLIs. Easy to code. Based on Python type hints." groups = ["default"] dependencies = [ - "click>=8.0.0", - "rich>=10.11.0", + "annotated-doc>=0.0.2", + "colorama; platform_system == \"Windows\"", + "rich>=13.8.0", "shellingham>=1.3.0", - "typing-extensions>=3.7.4.3", ] files = [ - {file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"}, - {file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"}, + {file = "typer-0.27.3-py3-none-any.whl", hash = "sha256:e50022f28b82a86313e54501317a1db64bf8f8d036ff8cfe5ca7e47675454aff"}, + {file = "typer-0.27.3.tar.gz", hash = "sha256:d0396f770a560ab1b0a8504e13b5f254b728cedb05c61cf0359e944e50ce8901"}, ] [[package]] @@ -942,35 +1317,38 @@ files = [ {file = "types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a"}, ] -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20241206" -requires_python = ">=3.8" -summary = "Typing stubs for python-dateutil" -groups = ["dev"] -files = [ - {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, - {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, -] - [[package]] name = "types-pyyaml" -version = "6.0.12.20241221" -requires_python = ">=3.8" +version = "6.0.12.20261030" +requires_python = ">=3.10" summary = "Typing stubs for PyYAML" groups = ["dev"] files = [ - {file = "types_PyYAML-6.0.12.20241221-py3-none-any.whl", hash = "sha256:0657a4ff8411a030a2116a196e8e008ea679696b5b1a8e1a6aa8ebb737b34688"}, - {file = "types_pyyaml-6.0.12.20241221.tar.gz", hash = "sha256:4f149aa893ff6a46889a30af4c794b23833014c469cc57cbc3ad77498a58996f"}, + {file = "types_pyyaml-6.0.12.20261030-py3-none-any.whl", hash = "sha256:508cc80546918201026a955356222111224e1f4f7deab0b91f2a95933f764e67"}, + {file = "types_pyyaml-6.0.12.20261030.tar.gz", hash = "sha256:9123d707aa31a3883eeb2cb290537c75246dfcb1c00abed7a70f5419d3bd31ae"}, ] [[package]] name = "typing-extensions" -version = "4.12.2" -requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.16.0" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" groups = ["default", "dev"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +requires_python = ">=3.10" +summary = "Runtime typing introspection tools" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.15.0", +] +files = [ + {file = "typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147"}, + {file = "typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47"}, ] diff --git a/pdm.minimal.lock b/pdm.minimal.lock index 8b9ef2698..018b1dfa5 100644 --- a/pdm.minimal.lock +++ b/pdm.minimal.lock @@ -4,41 +4,38 @@ [metadata] groups = ["default", "dev"] strategy = ["direct_minimal_versions", "inherit_metadata"] -lock_version = "4.5.0" -content_hash = "sha256:7f8035cfb12eec0bca9053eb34cc6b70a1cfbf464921c1abb3ae3c3e63cfeab4" +lock_version = "4.5.1" +content_hash = "sha256:de20848bca14d9110490150f1bf7f5e0d46935896a5c89d5fd353e9fcad3baa3" [[metadata.targets]] -requires_python = "~=3.9" +requires_python = "~=3.11" [[package]] name = "annotated-types" -version = "0.7.0" -requires_python = ">=3.8" +version = "0.8.0" +requires_python = ">=3.10" summary = "Reusable constraint types to use with typing.Annotated" groups = ["default"] -dependencies = [ - "typing-extensions>=4.0.0; python_version < \"3.9\"", -] 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"}, + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, ] [[package]] name = "anyio" -version = "3.7.1" -requires_python = ">=3.7" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" +version = "4.15.1" +requires_python = ">=3.10" +summary = "High-level concurrency and networking framework on top of asyncio or Trio" groups = ["default"] +marker = "sys_platform != \"emscripten\"" dependencies = [ - "exceptiongroup; python_version < \"3.11\"", + "exceptiongroup>=1.0.2; python_version < \"3.11\"", "idna>=2.8", - "sniffio>=1.1", - "typing-extensions; python_version < \"3.8\"", + "typing-extensions>=4.16.0; python_version < \"3.15\"", ] files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101"}, + {file = "anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94"}, ] [[package]] @@ -46,112 +43,21 @@ name = "attrs" version = "22.2.0" requires_python = ">=3.6" summary = "Classes Without Boilerplate" -groups = ["default"] +groups = ["default", "dev"] files = [ {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, ] -[[package]] -name = "certifi" -version = "2024.12.14" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." -groups = ["default"] -files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -requires_python = ">=3.7" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -groups = ["default"] -files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, -] - [[package]] name = "click" -version = "8.1.8" -requires_python = ">=3.7" +version = "8.5.0" +requires_python = ">=3.10" summary = "Composable command line interface toolkit" groups = ["default"] -dependencies = [ - "colorama; platform_system == \"Windows\"", - "importlib-metadata; python_version < \"3.8\"", -] files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, + {file = "click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360"}, + {file = "click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34"}, ] [[package]] @@ -160,7 +66,7 @@ version = "0.4.3" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" summary = "Cross-platform colored terminal text." groups = ["default", "dev"] -marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +marker = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, @@ -168,164 +74,238 @@ files = [ [[package]] name = "coverage" -version = "7.6.9" -requires_python = ">=3.9" +version = "7.17.0" +requires_python = ">=3.11" summary = "Code coverage measurement for Python" groups = ["dev"] files = [ - {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, - {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, - {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, - {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, - {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, - {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, - {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, - {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, - {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, - {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, - {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, - {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, - {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, - {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, - {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, - {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -requires_python = ">=3.7" -summary = "Backport of PEP 654 (exception groups)" -groups = ["default", "dev"] -marker = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "coverage-7.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99a7b390cbccc6bf029ca4da3f0f607c218bb4085e9cd6b58a90d14ca12993c3"}, + {file = "coverage-7.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f72640e02a470b57b15cd655820133f6892760209283a363a60e2df932d5f0fe"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2dc9233f39149b5a4f4aba17a4b6bc5cca22e52cfc4b06fdb07d014439b8b2ea"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e8113d3852e9377421a61de1917df27b3abb25404a4ecf69f9e64612c4345b5"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86a39539d10d666bb819c16c94d7edfc1aa2e0531b937f7c549b83d15177c6e"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:559f748446ee799803686b5784e3deb54c70d655ac386a860e06b11672504ba7"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d65c9e8e257f48a654908005046fbf1bc48d9540ae3776ad08c0acd010e10c"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b7fa0997b8eba7378ac66f31ec7f70164490353ab79b9667d37c0d365e50177"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6ae0453a3390e67f7bfbe34417bbdf5b4ebfc21db23fb446a0a41115eeeca6ca"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:96151262832f3c230ebae23b2d6911ad549e1738465946c97004913ff06112ba"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cd7397de6cc782f00fe5ac1d75e9b306bcfa49c8b6b8dd6ed1f7709393ad40b3"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d1fdf82c6fea57b5e9dada509f9ce9638c8334664e2c98c7f6f1ccf3d143ad2"}, + {file = "coverage-7.17.0-cp311-cp311-win32.whl", hash = "sha256:38c86e5d74bbfd88a393fa3f91257c1225c63aa6db6d5918de3e49ab04f91c6e"}, + {file = "coverage-7.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:2ed3097c6817bc1a7fc8ae0a6131db74aa1150d863cccc308237ff362d3226b2"}, + {file = "coverage-7.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:6052e878b628abcd38fa5f2b64abebc686eed4e2267da42434017a9c6d24f241"}, + {file = "coverage-7.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0cfdc938ca3319d873d1f1252e60620a39aee4ebf9dbd7cc58238109ebda9c51"}, + {file = "coverage-7.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d523da3c2e4bca49417768bcdb1a443c5b359dc44b8b93a3102e118fce912c40"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:adbf3450c0f3f2b2854e282a8758f2b3eddf787eeff24ec9c9bc2cbc38328cc1"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85932227e9dd38e62cd29c17fefdfecfb919eead6af1e3259522d03cf85de983"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f5477aac9c9e4a0a641c55f8f8fddd7f3bd91176c912c50f40ffced96b4034"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b8bb651a4406fb6a983a905309fec1783443c65e1771b41e92740a0f4a1176b"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0edeb6594f080c8de1263cccb239cb316ef77aeb959e2e0f38f2953fcc72a49e"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da94ec0484e84fce5c7bf8d0f54b97767a90d992653a129b35634699d516e0a5"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a25cc5e8c23a590815c6a7aa43624234d25cb23b21b484adb8a0daed59860db5"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db645c81cf4d6bcb392ea1894aad65d55d1588a8472c834b25ca575da3029a56"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6a80a11b90c4a03df1502304cf24ff953255b6b5d076ef79066ecf88baaff31f"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4eaf11d6b0f2147f46cfbf06fe7edcca6f095d0120da00ddc2458995ee869fc"}, + {file = "coverage-7.17.0-cp312-cp312-win32.whl", hash = "sha256:912a29f338798541d795e2e25903c6fe7a99dee70a40fe4706a9a124649b523e"}, + {file = "coverage-7.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:88a1a9fb0075b83389dd9b403bb3fce976031b3c2aa0e05e3e57143cf639de69"}, + {file = "coverage-7.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:2c680e838ff9e68d491158625ca2c1ddb17d78d791bba96a31f8b839b4990438"}, + {file = "coverage-7.17.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:84538c040f4e6214f663459d89372ce4e6b2bbdd9d397ef0dca7b3ecd56b8eb2"}, + {file = "coverage-7.17.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:6abfb6a4cf1cb1b67bc8fccdde177e2d2b781d0d34449e7078156547025140e5"}, + {file = "coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ef65e96f0fc1a7c8b7644c7011d47a8228555e7888ac4d6246f715a7f3d7c398"}, + {file = "coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:55939ea4c097fda2edabf80fac5e49bef15597b950a9f29fc56292a895245feb"}, + {file = "coverage-7.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bfb6f868af552c0ceb2d19220b8cbcc2be096ca26163ac30e682a13041433429"}, + {file = "coverage-7.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8d823d66afd59566030539c49c7a6acaf3083996682be2dda760c63555842094"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50678474e714c684b82c108e63fbb8f1bcf1266d8293a49980f1d08526f093b5"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:045c1eeb19fb4eae949850a761102fffb3a5f638b97f977912f95843e54f6669"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfb12891a773cbc2132349c77c690a045a882fe3f98b4bda50dfabb01893f9bd"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3067608b4429629f640c1b23d2f69b05d2fd45e00ebb6ee9e8fad220a6c11722"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1762e85cbbe3b5ff9c6dcbcc1b89d5c72320094292b7b9572bf882c85e428b7"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62bb77987d3f72a0aaa77c567ee7f9806bc2e748c8f46fafbe1fdb90dd148398"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6cc31f27240d4e080437e3b8229d92f1d809c9a4f233607385373b73974e183e"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61cde5222f1933d476647c5a25ed1a8678b7703098cb5a15f4a0aa39967d8c28"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11f5f7fb2d97a0fe865f5e7bad4eb0619b957637f676bc088d13844adf26ba95"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e09612b1b82c9df1d2b6c85dee88d5aad3e73d77a8ec0b16ac6a7322cbdf5d9"}, + {file = "coverage-7.17.0-cp313-cp313-win32.whl", hash = "sha256:bd3e9eb493631ff1ca34bf5efac25b5c66724f837bf6182c4fc771c55946423d"}, + {file = "coverage-7.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1fc9a7cea899c51d8a53e66c2f082a3d9e461ece32418d3ef0a0b90c32bfd3d"}, + {file = "coverage-7.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:e65a1e61a68493fbfbe44d8bec71b3231d81b9917f62d3dafe0e105072147e83"}, + {file = "coverage-7.17.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:dc5936491e306f2f6f095bd5fba593de11eb0375cee787ca1f245c3a333a523a"}, + {file = "coverage-7.17.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:968c5fb6e0c83cab752d79a70209648ce4d90cae815c60fa229b59e264601062"}, + {file = "coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:12cb8a20ad904a5811e6660d829a586b216e5254b9b34b76dde8a4d65d827dc6"}, + {file = "coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bb5ca66937c55bc85d1a5e9c5900a3e4693a8edf2280a9b3672b7b2661a84b95"}, + {file = "coverage-7.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5cc15faaf318ee5040d835a62c75bc6f122d837274f3b5efd47bf9fbd4ac5b85"}, + {file = "coverage-7.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d6d9d396c8b7bcaf76d774a10140b218111484e54c73a5f2a1b75655381748e"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:42e6f170d1ac04b7eb3363310a6c41cdfd82c1fe6bf9ffbc4a86f380bdf091c7"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:acdb5f3c96046efff9dc6c572a6acb2b8d0da09372d7b9a9ed7249cf7d17434f"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3393209f6894713161ee82a9b0e673e58237e0f9733c6638e7a6c0c620ef0b33"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9494d9e99389d39e6a96227749af898d8924ff591cb63944f8a98e68582bf0e"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:032d4c2db4c677eb71d43f2f8b80bf64e9346f7a3f7b6ebb13f1782f3cc6c274"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4343a9378b964eb3cb80fa90406385de91c7e982a769ca9314d848f93bc0d3f2"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b609df3d6d900464207532c5170a65c6ede8d2e0b91f151f90008a866395cd76"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a01ef92caeedfa4f2ad171a3a8e6fece4565de871d8bca428151bf9929f41048"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a3914e2c98c34b94c26d2add3d2a61ccfd3534fc4eaa879ee290bdbb5184ba6c"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8a72b26dc5c9a9422e45472b1d67ba5caf273c524c8668cc04b23062663755e"}, + {file = "coverage-7.17.0-cp314-cp314-win32.whl", hash = "sha256:03baf0688e14a5a44ffd1eefa8928dfc92b14565e521174e072b31b971aa1657"}, + {file = "coverage-7.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3a4459b0f389dd1e51aac3dbfc4ca35b30cae7dc99caa3c0a086d1997a985709"}, + {file = "coverage-7.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32ee5831b15ba239bef5d272ae586ca8acea4a661b5f2941f5a0d380e518b23"}, + {file = "coverage-7.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9c083e253e96fdeeaf48133fe836cac86ace2db215d235700e7f7532420a200c"}, + {file = "coverage-7.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b88fb4d4d7608cbcebdc7ff7dbffc2115e3b81d6c33a79147cd79d54fcb080f4"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2bf3ee38a40c0570c316b9e7507c472469586906a20f289705898528c31aaea8"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c570293f001ea91598b90e90cb9b98fddee833e90bbede432cbd218331ccd8e"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:791f9ee09814dd30d9562b4f4eb037a371485ec1c830b53999d2320d20c4e764"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7c5672ecf1b497de7a0b37743bef40e3b58e3207e9fe2ed6682cb0420836a88"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:337f87e62e1c5f43c8198d879f677c342d152f84d3f157add12558853cbb8eaf"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c8a58baa8eb6154c6645eadcc19b6c089deab78c24a612a83206f2a8a5992a7c"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb5745048ee20f796b0e3ab61831049e749acff188563e2af32f58ede957ac1b"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:7b5abcae74750302ac4bc77351eaee87c450e4abd3b6225eacde3858e260f8b2"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4e153f0bd6509dbf837fd585f489b3328bfd98f2a718a0b15ae63032148b285c"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e42a835271922fe7bf98c7a05c78d4032f4e52bf5b07fafa1eadfdfc2f0ad22f"}, + {file = "coverage-7.17.0-cp314-cp314t-win32.whl", hash = "sha256:5900055351ec89fdffb34b27898815106529ded29a9496464c1ebd6313fd40fa"}, + {file = "coverage-7.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5ee9c8bd13a59e301dc2aefe7aa8384633406808de051b1982775667094eb673"}, + {file = "coverage-7.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a9cb9a0fb58e6fff5442969af36094ecd6976f4907f179e33b27e15355d8f74"}, + {file = "coverage-7.17.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:adabc1079ba844b096799ba8638d8df434aca1613f95bb7d5791af320cf05559"}, + {file = "coverage-7.17.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:6b4593d2871c4c801656dd5861e759f8f5156febd6b6061b02f6a72d64618b7b"}, + {file = "coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c2c48a36bb970b7265b196032bfd4237c0b373f2b25ef7012d7e905b5b35c19c"}, + {file = "coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f8bc2cd4c6aa58be1ca243fb8cddb6c3c42031fe4403991ad9cf2b3baed03d9f"}, + {file = "coverage-7.17.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:cbf0b83ce927de6053279ba61342d9f8892afde1385fcba9144cd05d4023d109"}, + {file = "coverage-7.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:66efc0fb99cee34626fc17d3adac9c3cd816bfdad563bb1e983c3ef432f566d4"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58a304efdf2253fb2ea03d45d0a04cc174a92a0cd5279c28c5bb893ce7410968"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aed06129a9f530a948928260d7d2d81c47261355b0127e3c782251b38958463c"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6cdb47a9be2d3313829bed1d296086287682a3ee901ff987b3432b45705b771"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:804fbaf81d09b34e2f8b1159d01e211a50713d743d32fd9b2ca6cf0ae507a66f"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2afebb96da9a1bbca142f8cc12320c14e5c215e0a49cded4f191f5a31506a28"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bd04fd23206be9ad3de13b0b39313094fafa66a1f6baf70f5471c004a9fac47d"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:5ebfbc87836529a31d25c2dec1262c38eea852ec1c46c0f690a515a7339d9c5b"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:a59c3b470ac3b2432af3598a996affbeb10236cd899a005c1c4e517fcff8b592"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:7fef12cec4b96f59e09761cbd4fc9bdf4788062d1ae435b920241fc16c8a9549"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a14e8d69464da9cbe1dc29e7116dbd37296d1d51a695327c478e250349085089"}, + {file = "coverage-7.17.0-cp315-cp315-win32.whl", hash = "sha256:9a66074231799d8884d267234d49f60c90b43a945dfe069646b72f3058cd4e67"}, + {file = "coverage-7.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:79cf71e5f9a48877ccfed5baa2c72f45af4feeaa596249e23eb15b473134830e"}, + {file = "coverage-7.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:09e563b49c81cd0a33734e926d7a4a6ef1778d74304cde2dcc3aff8b30ea1036"}, + {file = "coverage-7.17.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a5e2e0625dfc789af0b2e10acfc106b352f314fffbd19e6392a7961c8cbe5d9a"}, + {file = "coverage-7.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:194562ab1eff0d85aee8a17ce4c5c0bc1ebf70a629a7145257123156ad100fd6"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:85682ca89bc4601125dcdff23fa70e6274e13c9ad502fba845e6c274ab2c6569"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0d0fac604a962ff546beb038cba9a5a09d88e6e3f01612fb46f7bd4fe5dc8dca"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72edd4f9483ae30cd1ba4f230687840ef7b58a16afb1c9e81131fd6d64655f36"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91e487ad07c511da77b574820141a7c2da09bc2a4b4090b5be2424295d100697"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1cabb31531002fef5197de5df3c84730aef01e750e25154b9813a2583435aba2"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c9ac886242bbd50fb35a2242ba79555a99891e657da33f98b7ca968e3488a350"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d45cdc495671fc99268ff3653e0da4244972e9fc0e1fc3ea9de18781ad28963a"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:12961f738d717a046d4bf3f8d1727eb3ebe2ed5478ff96cd6233d4dfc39323d3"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1ee0a982b0e1230b3edf768a7dd022bd2377303763895327ac62deb5efacee03"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:460679f53893081edb94eeed011d26be37228ba649b853fd2aed7e4394fb1f35"}, + {file = "coverage-7.17.0-cp315-cp315t-win32.whl", hash = "sha256:600e121e412cd6bdb52ec105b95c6a04da0c9710d1534f6e64a42db667907802"}, + {file = "coverage-7.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c3e11b81faafb1f063248735c3e2c5c4183fc1db2c7f46782170dd7760dffcbc"}, + {file = "coverage-7.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d472ee1c64868a917a47f02ffb4ac8c22b477f7589d57a3eb0c58b1ad4f0137c"}, + {file = "coverage-7.17.0-py3-none-any.whl", hash = "sha256:bac487dce0d2aff28ddf1a533a916f771416eee0e493960f611e49037b878e9d"}, + {file = "coverage-7.17.0.tar.gz", hash = "sha256:cc010a0a7fbdd9bb8bdfd0d3f04a5151275860213cbd7e2b876b29778489c712"}, ] [[package]] name = "execnet" -version = "2.1.1" +version = "2.1.2" requires_python = ">=3.8" summary = "execnet: rapid multi-Python deployment" groups = ["dev"] files = [ - {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, - {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, + {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, + {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, ] [[package]] name = "h11" -version = "0.12.0" -requires_python = ">=3.6" +version = "0.16.0" +requires_python = ">=3.8" summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" groups = ["default"] +marker = "sys_platform != \"emscripten\"" files = [ - {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, - {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, + {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 = "0.13.7" -requires_python = ">=3.6" +name = "httpcore2" +version = "2.13.1" +requires_python = ">=3.10" summary = "A minimal low-level HTTP client." groups = ["default"] +marker = "sys_platform != \"emscripten\"" dependencies = [ - "anyio==3.*", - "h11<0.13,>=0.11", - "sniffio==1.*", + "h11>=0.16", + "truststore>=0.10", ] files = [ - {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"}, - {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"}, + {file = "httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a"}, + {file = "httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103"}, ] [[package]] -name = "httpx" -version = "0.20.0" -requires_python = ">=3.6" +name = "httpx2" +version = "2.13.1" +requires_python = ">=3.10" summary = "The next generation HTTP client." groups = ["default"] dependencies = [ - "async-generator; python_version < \"3.7\"", - "certifi", - "charset-normalizer", - "httpcore<0.14.0,>=0.13.3", - "rfc3986[idna2008]<2,>=1.3", - "sniffio", + "anyio>=4.10; sys_platform != \"emscripten\"", + "httpcore2==2.13.1; sys_platform != \"emscripten\"", + "httpx2-jsfetch; sys_platform == \"emscripten\" and python_version >= \"3.12\"", + "idna>=3.18", + "truststore>=0.10; sys_platform != \"emscripten\"", + "typing-extensions>=4.5.0; python_version < \"3.13\"", ] files = [ - {file = "httpx-0.20.0-py3-none-any.whl", hash = "sha256:33af5aad9bdc82ef1fc89219c1e36f5693bf9cd0ebe330884df563445682c0f8"}, - {file = "httpx-0.20.0.tar.gz", hash = "sha256:09606d630f070d07f9ff28104fbcea429ea0014c1e89ac90b4d8de8286c40e7b"}, + {file = "httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4"}, + {file = "httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa"}, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +requires_python = ">=3.12" +summary = "httpx2 transports for Emscripten/Pyodide, backed by the JavaScript fetch API." +groups = ["default"] +marker = "sys_platform == \"emscripten\" and python_version >= \"3.12\"" +files = [ + {file = "httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32"}, + {file = "httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60"}, +] + +[[package]] +name = "hypothesis" +version = "6.100.0" +requires_python = ">=3.8" +summary = "A library for property-based testing" +groups = ["dev"] +dependencies = [ + "attrs>=22.2.0", + "exceptiongroup>=1.0.0; python_version < \"3.11\"", + "sortedcontainers<3.0.0,>=2.1.0", +] +files = [ + {file = "hypothesis-6.100.0-py3-none-any.whl", hash = "sha256:ceaeb7c051085dbec37f2fc4dca524b6304472ff1887fed53b3d84705507c10e"}, + {file = "hypothesis-6.100.0.tar.gz", hash = "sha256:1841f6b5083844cd4b66965e44a17c0dc8fe8e9c6370c1f7b8d50647fcb2efd3"}, ] [[package]] name = "idna" -version = "3.10" -requires_python = ">=3.6" +version = "3.20" +requires_python = ">=3.9" summary = "Internationalized Domain Names in Applications (IDNA)" groups = ["default"] files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, + {file = "idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c"}, + {file = "idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44"}, ] [[package]] name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" +version = "2.3.1" +requires_python = ">=3.10" summary = "brain-dead simple config-ini parsing" groups = ["dev"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7"}, + {file = "iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960"}, ] [[package]] @@ -342,74 +322,159 @@ files = [ {file = "Jinja2-3.0.0.tar.gz", hash = "sha256:ea8d7dd814ce9df6de6a761ec7f1cac98afe305b8cdc4aaae4e114b8d8ce24c5"}, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +requires_python = ">=3.10" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, +] + [[package]] name = "markupsafe" -version = "3.0.2" +version = "3.0.4" requires_python = ">=3.9" summary = "Safely add untrusted strings to HTML/XML markup." groups = ["default"] files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, + {file = "markupsafe-3.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346"}, + {file = "markupsafe-3.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7"}, + {file = "markupsafe-3.0.4-cp311-cp311-win32.whl", hash = "sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5"}, + {file = "markupsafe-3.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3"}, + {file = "markupsafe-3.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e"}, + {file = "markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6"}, + {file = "markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e"}, + {file = "markupsafe-3.0.4-cp312-cp312-win32.whl", hash = "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237"}, + {file = "markupsafe-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7"}, + {file = "markupsafe-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9"}, + {file = "markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1"}, + {file = "markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e"}, + {file = "markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248"}, + {file = "markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17"}, + {file = "markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0"}, + {file = "markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5"}, + {file = "markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc"}, + {file = "markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed"}, + {file = "markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6"}, + {file = "markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634"}, + {file = "markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4"}, + {file = "markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c"}, + {file = "markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe"}, + {file = "markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a"}, + {file = "markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2"}, + {file = "markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c"}, + {file = "markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65"}, + {file = "markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429"}, + {file = "markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8"}, + {file = "markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741"}, + {file = "markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b"}, + {file = "markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214"}, + {file = "markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67"}, + {file = "markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad"}, + {file = "markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba"}, + {file = "markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] @@ -424,11 +489,6 @@ dependencies = [ "typing-extensions>=4.6.0", ] 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"}, @@ -444,46 +504,41 @@ files = [ {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-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]] name = "mypy-extensions" -version = "1.0.0" -requires_python = ">=3.5" +version = "1.1.0" +requires_python = ">=3.8" summary = "Type system extensions for programs checked with the mypy type checker." groups = ["dev"] files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, + {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 = "24.2" -requires_python = ">=3.8" +version = "26.3" +requires_python = ">=3.9" summary = "Core utilities for Python packages" groups = ["dev"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] name = "pluggy" -version = "1.5.0" -requires_python = ">=3.8" +version = "1.7.0" +requires_python = ">=3.10" summary = "plugin and hook calling mechanisms for python" groups = ["dev"] files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec"}, + {file = "pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8"}, ] [[package]] @@ -499,93 +554,84 @@ files = [ [[package]] name = "pydantic" -version = "2.1.1" -requires_python = ">=3.7" +version = "2.10.0" +requires_python = ">=3.8" summary = "Data validation using Python type hints" groups = ["default"] dependencies = [ - "annotated-types>=0.4.0", - "pydantic-core==2.4.0", - "typing-extensions>=4.6.1", + "annotated-types>=0.6.0", + "pydantic-core==2.27.0", + "typing-extensions>=4.12.2", ] files = [ - {file = "pydantic-2.1.1-py3-none-any.whl", hash = "sha256:43bdbf359d6304c57afda15c2b95797295b702948082d4c23851ce752f21da70"}, - {file = "pydantic-2.1.1.tar.gz", hash = "sha256:22d63db5ce4831afd16e7c58b3192d3faf8f79154980d9397d9867254310ba4b"}, + {file = "pydantic-2.10.0-py3-none-any.whl", hash = "sha256:5e7807ba9201bdf61b1b58aa6eb690916c40a47acfb114b1b4fef3e7fd5b30fc"}, + {file = "pydantic-2.10.0.tar.gz", hash = "sha256:0aca0f045ff6e2f097f1fe89521115335f15049eeb8a7bef3dafe4b19a74e289"}, ] [[package]] name = "pydantic-core" -version = "2.4.0" -requires_python = ">=3.7" -summary = "" +version = "2.27.0" +requires_python = ">=3.8" +summary = "Core functionality for Pydantic validation and serialization" groups = ["default"] dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.4.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:2ca4687dd996bde7f3c420def450797feeb20dcee2b9687023e3323c73fc14a2"}, - {file = "pydantic_core-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:782fced7d61469fd1231b184a80e4f2fa7ad54cd7173834651a453f96f29d673"}, - {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6213b471b68146af97b8551294e59e7392c2117e28ffad9c557c65087f4baee3"}, - {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63797499a219d8e81eb4e0c42222d0a4c8ec896f5c76751d4258af95de41fdf1"}, - {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_24_armv7l.whl", hash = "sha256:0455876d575a35defc4da7e0a199596d6c773e20d3d42fa1fc29f6aa640369ed"}, - {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:8c938c96294d983dcf419b54dba2d21056959c22911d41788efbf949a29ae30d"}, - {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_24_s390x.whl", hash = "sha256:878a5017d93e776c379af4e7b20f173c82594d94fa073059bcc546789ad50bf8"}, - {file = "pydantic_core-2.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:69159afc2f2dc43285725f16143bc5df3c853bc1cb7df6021fce7ef1c69e8171"}, - {file = "pydantic_core-2.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54df7df399b777c1fd144f541c95d351b3aa110535a6810a6a569905d106b6f3"}, - {file = "pydantic_core-2.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e412607ca89a0ced10758dfb8f9adcc365ce4c1c377e637c01989a75e9a9ec8a"}, - {file = "pydantic_core-2.4.0-cp310-none-win32.whl", hash = "sha256:853f103e2b9a58832fdd08a587a51de8b552ae90e1a5d167f316b7eabf8d7dde"}, - {file = "pydantic_core-2.4.0-cp310-none-win_amd64.whl", hash = "sha256:3ba2c9c94a9176f6321a879c8b864d7c5b12d34f549a4c216c72ce213d7d953c"}, - {file = "pydantic_core-2.4.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:a8b7acd04896e8f161e1500dc5f218017db05c1d322f054e89cbd089ce5d0071"}, - {file = "pydantic_core-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16468bd074fa4567592d3255bf25528ed41e6b616d69bf07096bdb5b66f947d1"}, - {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cba5ad5eef02c86a1f3da00544cbc59a510d596b27566479a7cd4d91c6187a11"}, - {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7206e41e04b443016e930e01685bab7a308113c0b251b3f906942c8d4b48fcb"}, - {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_24_armv7l.whl", hash = "sha256:c1375025f0bfc9155286ebae8eecc65e33e494c90025cda69e247c3ccd2bab00"}, - {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:3534118289e33130ed3f1cc487002e8d09b9f359be48b02e9cd3de58ce58fba9"}, - {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_24_s390x.whl", hash = "sha256:94d2b36a74623caab262bf95f0e365c2c058396082bd9d6a9e825657d0c1e7fa"}, - {file = "pydantic_core-2.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af24ad4fbaa5e4a2000beae0c3b7fd1c78d7819ab90f9370a1cfd8998e3f8a3c"}, - {file = "pydantic_core-2.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bf10963d8aed8bbe0165b41797c9463d4c5c8788ae6a77c68427569be6bead41"}, - {file = "pydantic_core-2.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68199ada7c310ddb8c76efbb606a0de656b40899388a7498954f423e03fc38be"}, - {file = "pydantic_core-2.4.0-cp311-none-win32.whl", hash = "sha256:6f855bcc96ed3dd56da7373cfcc9dcbabbc2073cac7f65c185772d08884790ce"}, - {file = "pydantic_core-2.4.0-cp311-none-win_amd64.whl", hash = "sha256:de39eb3bab93a99ddda1ac1b9aa331b944d8bcc4aa9141148f7fd8ee0299dafc"}, - {file = "pydantic_core-2.4.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:f773b39780323a0499b53ebd91a28ad11cde6705605d98d999dfa08624caf064"}, - {file = "pydantic_core-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a297c0d6c61963c5c3726840677b798ca5b7dfc71bc9c02b9a4af11d23236008"}, - {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:546064c55264156b973b5e65e5fafbe5e62390902ce3cf6b4005765505e8ff56"}, - {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36ba9e728588588f0196deaf6751b9222492331b5552f865a8ff120869d372e0"}, - {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_24_armv7l.whl", hash = "sha256:57a53a75010c635b3ad6499e7721eaa3b450e03f6862afe2dbef9c8f66e46ec8"}, - {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_24_ppc64le.whl", hash = "sha256:4b262bbc13022f2097c48a21adcc360a81d83dc1d854c11b94953cd46d7d3c07"}, - {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_24_s390x.whl", hash = "sha256:01947ad728f426fa07fcb26457ebf90ce29320259938414bc0edd1476e75addb"}, - {file = "pydantic_core-2.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b2799c2eaf182769889761d4fb4d78b82bc47dae833799fedbf69fc7de306faa"}, - {file = "pydantic_core-2.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a08fd490ba36d1fbb2cd5dcdcfb9f3892deb93bd53456724389135712b5fc735"}, - {file = "pydantic_core-2.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1e8a7c62d15a5c4b307271e4252d76ebb981d6251c6ecea4daf203ef0179ea4f"}, - {file = "pydantic_core-2.4.0-cp312-none-win32.whl", hash = "sha256:9206c14a67c38de7b916e486ae280017cf394fa4b1aa95cfe88621a4e1d79725"}, - {file = "pydantic_core-2.4.0-cp312-none-win_amd64.whl", hash = "sha256:884235507549a6b2d3c4113fb1877ae263109e787d9e0eb25c35982ab28d0399"}, - {file = "pydantic_core-2.4.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:867d3eea954bea807cabba83cfc939c889a18576d66d197c60025b15269d7cc0"}, - {file = "pydantic_core-2.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:664402ef0c238a7f8a46efb101789d5f2275600fb18114446efec83cfadb5b66"}, - {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64e8012ad60a5f0da09ed48725e6e923d1be25f2f091a640af6079f874663813"}, - {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac2b680de398f293b68183317432b3d67ab3faeba216aec18de0c395cb5e3060"}, - {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_24_armv7l.whl", hash = "sha256:8efc1be43b036c2b6bcfb1451df24ee0ddcf69c31351003daf2699ed93f5687b"}, - {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:d93aedbc4614cc21b9ab0d0c4ccd7143354c1f7cffbbe96ae5216ad21d1b21b5"}, - {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_24_s390x.whl", hash = "sha256:af788b64e13d52fc3600a68b16d31fa8d8573e3ff2fc9a38f8a60b8d94d1f012"}, - {file = "pydantic_core-2.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97c6349c81cee2e69ef59eba6e6c08c5936e6b01c2d50b9e4ac152217845ae09"}, - {file = "pydantic_core-2.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc086ddb6dc654a15deeed1d1f2bcb1cb924ebd70df9dca738af19f64229b06c"}, - {file = "pydantic_core-2.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e953353180bec330c3b830891d260b6f8e576e2d18db3c78d314e56bb2276066"}, - {file = "pydantic_core-2.4.0-cp39-none-win32.whl", hash = "sha256:6feb4b64d11d5420e517910d60a907d08d846cacaf4e029668725cd21d16743c"}, - {file = "pydantic_core-2.4.0-cp39-none-win_amd64.whl", hash = "sha256:153a61ac4030fa019b70b31fb7986461119230d3ba0ab661c757cfea652f4332"}, - {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3fcf529382b282a30b466bd7af05be28e22aa620e016135ac414f14e1ee6b9e1"}, - {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2edef05b63d82568b877002dc4cb5cc18f8929b59077120192df1e03e0c633f8"}, - {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da055a1b0bfa8041bb2ff586b2cb0353ed03944a3472186a02cc44a557a0e661"}, - {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:77dadc764cf7c5405e04866181c5bd94a447372a9763e473abb63d1dfe9b7387"}, - {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a4ea23b07f29487a7bef2a869f68c7ee0e05424d81375ce3d3de829314c6b5ec"}, - {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:382f0baa044d674ad59455a5eff83d7965572b745cc72df35c52c2ce8c731d37"}, - {file = "pydantic_core-2.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:08f89697625e453421401c7f661b9d1eb4c9e4c0a12fd256eeb55b06994ac6af"}, - {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:efff8b6761a1f6e45cebd1b7a6406eb2723d2d5710ff0d1b624fe11313693989"}, - {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a1e0352558cd7ccc014ffe818c7d87b15ec6145875e2cc5fa4bb7351a1033d"}, - {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a027f41c5008571314861744d83aff75a34cf3a07022e0be32b214a5bc93f7f1"}, - {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1927f0e15d190f11f0b8344373731e28fd774c6d676d8a6cfadc95c77214a48b"}, - {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7aa82d483d5fb867d4fb10a138ffd57b0f1644e99f2f4f336e48790ada9ada5e"}, - {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b85778308bf945e9b33ac604e6793df9b07933108d20bdf53811bc7c2798a4af"}, - {file = "pydantic_core-2.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3ded19dcaefe2f6706d81e0db787b59095f4ad0fbadce1edffdf092294c8a23f"}, - {file = "pydantic_core-2.4.0.tar.gz", hash = "sha256:ec3473c9789cc00c7260d840c3db2c16dbfc816ca70ec87a00cddfa3e1a1cdd5"}, + {file = "pydantic_core-2.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4523c4009c3f39d948e01962223c9f5538602e7087a628479b723c939fab262d"}, + {file = "pydantic_core-2.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84af1cf7bfdcbc6fcf5a5f70cc9896205e0350306e4dd73d54b6a18894f79386"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e65466b31be1070b4a5b7dbfbd14b247884cb8e8b79c64fb0f36b472912dbaea"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5c022bb0d453192426221605efc865373dde43b17822a264671c53b068ac20c"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bb69bf3b6500f195c3deb69c1205ba8fc3cb21d1915f1f158a10d6b1ef29b6a"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aa4d1b2eba9a325897308b3124014a142cdccb9f3e016f31d3ebee6b5ea5e75"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e96ca781e0c01e32115912ebdf7b3fb0780ce748b80d7d28a0802fa9fbaf44e"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b872c86d8d71827235c7077461c502feb2db3f87d9d6d5a9daa64287d75e4fa0"}, + {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:82e1ad4ca170e8af4c928b67cff731b6296e6a0a0981b97b2eb7c275cc4e15bd"}, + {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:eb40f828bc2f73f777d1eb8fee2e86cd9692a4518b63b6b5aa8af915dfd3207b"}, + {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9a8fbf506fde1529a1e3698198fe64bfbe2e0c09557bc6a7dcf872e7c01fec40"}, + {file = "pydantic_core-2.27.0-cp311-none-win32.whl", hash = "sha256:24f984fc7762ed5f806d9e8c4c77ea69fdb2afd987b4fd319ef06c87595a8c55"}, + {file = "pydantic_core-2.27.0-cp311-none-win_amd64.whl", hash = "sha256:68950bc08f9735306322bfc16a18391fcaac99ded2509e1cc41d03ccb6013cfe"}, + {file = "pydantic_core-2.27.0-cp311-none-win_arm64.whl", hash = "sha256:3eb8849445c26b41c5a474061032c53e14fe92a11a5db969f722a2716cd12206"}, + {file = "pydantic_core-2.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8117839a9bdbba86e7f9df57018fe3b96cec934c3940b591b0fd3fbfb485864a"}, + {file = "pydantic_core-2.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a291d0b4243a259c8ea7e2b84eb9ccb76370e569298875a7c5e3e71baf49057a"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e35afd9e10b2698e6f2f32256678cb23ca6c1568d02628033a837638b3ed12"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ab0d979c969983cdb97374698d847a4acffb217d543e172838864636ef10d9"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d06b667e53320332be2bf6f9461f4a9b78092a079b8ce8634c9afaa7e10cd9f"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78f841523729e43e3928a364ec46e2e3f80e6625a4f62aca5c345f3f626c6e8a"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:400bf470e4327e920883b51e255617dfe4496d4e80c3fea0b5a5d0bf2c404dd4"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:951e71da6c89d354572098bada5ba5b5dc3a9390c933af8a614e37755d3d1840"}, + {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a51ce96224eadd1845150b204389623c8e129fde5a67a84b972bd83a85c6c40"}, + {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:483c2213a609e7db2c592bbc015da58b6c75af7360ca3c981f178110d9787bcf"}, + {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:359e7951f04ad35111b5ddce184db3391442345d0ab073aa63a95eb8af25a5ef"}, + {file = "pydantic_core-2.27.0-cp312-none-win32.whl", hash = "sha256:ee7d9d5537daf6d5c74a83b38a638cc001b648096c1cae8ef695b0c919d9d379"}, + {file = "pydantic_core-2.27.0-cp312-none-win_amd64.whl", hash = "sha256:2be0ad541bb9f059954ccf8877a49ed73877f862529575ff3d54bf4223e4dd61"}, + {file = "pydantic_core-2.27.0-cp312-none-win_arm64.whl", hash = "sha256:6e19401742ed7b69e51d8e4df3c03ad5ec65a83b36244479fd70edde2828a5d9"}, + {file = "pydantic_core-2.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5f2b19b8d6fca432cb3acf48cf5243a7bf512988029b6e6fd27e9e8c0a204d85"}, + {file = "pydantic_core-2.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c86679f443e7085ea55a7376462553996c688395d18ef3f0d3dbad7838f857a2"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510b11e9c3b1a852876d1ccd8d5903684336d635214148637ceb27366c75a467"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb704155e73b833801c247f39d562229c0303f54770ca14fb1c053acb376cf10"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ce048deb1e033e7a865ca384770bccc11d44179cf09e5193a535c4c2f497bdc"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58560828ee0951bb125c6f2862fbc37f039996d19ceb6d8ff1905abf7da0bf3d"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb4785894936d7682635726613c44578c420a096729f1978cd061a7e72d5275"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2883b260f7a93235488699d39cbbd94fa7b175d3a8063fbfddd3e81ad9988cb2"}, + {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6fcb3fa3855d583aa57b94cf146f7781d5d5bc06cb95cb3afece33d31aac39b"}, + {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e851a051f7260e6d688267eb039c81f05f23a19431bd7dfa4bf5e3cb34c108cd"}, + {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edb1bfd45227dec8d50bc7c7d86463cd8728bcc574f9b07de7369880de4626a3"}, + {file = "pydantic_core-2.27.0-cp313-none-win32.whl", hash = "sha256:678f66462058dd978702db17eb6a3633d634f7aa0deaea61e0a674152766d3fc"}, + {file = "pydantic_core-2.27.0-cp313-none-win_amd64.whl", hash = "sha256:d28ca7066d6cdd347a50d8b725dc10d9a1d6a1cce09836cf071ea6a2d4908be0"}, + {file = "pydantic_core-2.27.0-cp313-none-win_arm64.whl", hash = "sha256:6f4a53af9e81d757756508b57cae1cf28293f0f31b9fa2bfcb416cc7fb230f9d"}, + {file = "pydantic_core-2.27.0.tar.gz", hash = "sha256:f57783fbaf648205ac50ae7d646f27582fc706be3977e87c3c124e7a92407b10"}, +] + +[[package]] +name = "pygments" +version = "2.21.0" +requires_python = ">=3.9" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["default"] +files = [ + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, ] [[package]] @@ -637,8 +683,8 @@ files = [ [[package]] name = "pytest-xdist" -version = "3.6.1" -requires_python = ">=3.8" +version = "3.8.0" +requires_python = ">=3.9" summary = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" groups = ["dev"] dependencies = [ @@ -646,22 +692,8 @@ dependencies = [ "pytest>=7.0.0", ] files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, -] - -[[package]] -name = "python-dateutil" -version = "2.8.1" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -groups = ["default"] -dependencies = [ - "six>=1.5", -] -files = [ - {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, - {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, + {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]] @@ -674,28 +706,18 @@ files = [ ] [[package]] -name = "rfc3986" -version = "1.5.0" -summary = "Validating URI References per RFC 3986" -groups = ["default"] -files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] - -[[package]] -name = "rfc3986" -version = "1.5.0" -extras = ["idna2008"] -summary = "Validating URI References per RFC 3986" +name = "rich" +version = "15.0.0" +requires_python = ">=3.9.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" groups = ["default"] dependencies = [ - "idna", - "rfc3986==1.5.0", + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", ] files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, ] [[package]] @@ -703,7 +725,7 @@ name = "ruamel-yaml" version = "0.18.6" requires_python = ">=3.7" summary = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -groups = ["default", "dev"] +groups = ["default"] dependencies = [ "ruamel-yaml-clib>=0.2.7; platform_python_implementation == \"CPython\" and python_version < \"3.13\"", ] @@ -714,72 +736,53 @@ files = [ [[package]] name = "ruamel-yaml-clib" -version = "0.2.12" +version = "0.2.15" requires_python = ">=3.9" summary = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -groups = ["default", "dev"] +groups = ["default"] marker = "platform_python_implementation == \"CPython\" and python_version < \"3.13\"" files = [ - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, - {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, -] - -[[package]] -name = "ruamel-yaml-string" -version = "0.1.1" -requires_python = ">=3" -summary = "add dump_to_string/dumps method that returns YAML document as string" -groups = ["dev"] -dependencies = [ - "ruamel-yaml>=0.17.17", -] -files = [ - {file = "ruamel.yaml.string-0.1.1-py3-none-any.whl", hash = "sha256:eb146bcb42b116216638034a434e9cf3ae2a5d3933aa37183a9854b5f3ff42de"}, - {file = "ruamel.yaml.string-0.1.1.tar.gz", hash = "sha256:7a7aedcc055d45c004d38b756f58474ebefb106851f4ce56ce58415709784350"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c"}, + {file = "ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144"}, + {file = "ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec"}, + {file = "ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa"}, + {file = "ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467"}, + {file = "ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600"}, ] [[package]] @@ -787,7 +790,7 @@ name = "ruff" version = "0.2.0" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." -groups = ["default"] +groups = ["default", "dev"] files = [ {file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:638ea3294f800d18bae84a492cb5a245c8d29c90d19a91d8e338937a4c27fca0"}, {file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff35433fcf4dff6d610738712152df6b7d92351a1bde8e00bd405b08b3d5759"}, @@ -820,25 +823,13 @@ files = [ ] [[package]] -name = "six" -version = "1.17.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Python 2 and 3 compatibility utilities" -groups = ["default"] -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 = "sniffio" -version = "1.3.1" -requires_python = ">=3.7" -summary = "Sniff out which async library your code is running under" -groups = ["default"] +name = "sortedcontainers" +version = "2.4.0" +summary = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +groups = ["dev"] files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] [[package]] @@ -856,59 +847,32 @@ files = [ ] [[package]] -name = "tomli" -version = "2.2.1" -requires_python = ">=3.8" -summary = "A lil' TOML parser" -groups = ["dev"] -marker = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +name = "truststore" +version = "0.10.5" +requires_python = ">=3.10" +summary = "Verify certificates using native system trust stores" +groups = ["default"] +marker = "sys_platform != \"emscripten\"" +files = [ + {file = "truststore-0.10.5-py3-none-any.whl", hash = "sha256:9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c"}, + {file = "truststore-0.10.5.tar.gz", hash = "sha256:30d36967ccaded5cbb38d602c433f53600036c79d502f4533a49b60a03bbefcd"}, ] [[package]] name = "typer" -version = "0.6.1" -requires_python = ">=3.6" +version = "0.16.1" +requires_python = ">=3.7" summary = "Typer, build great CLIs. Easy to code. Based on Python type hints." groups = ["default"] dependencies = [ - "click<9.0.0,>=7.1.1", + "click>=8.0.0", + "rich>=10.11.0", + "shellingham>=1.3.0", + "typing-extensions>=3.7.4.3", ] files = [ - {file = "typer-0.6.1-py3-none-any.whl", hash = "sha256:54b19e5df18654070a82f8c2aa1da456a4ac16a2a83e6dcd9f170e291c56338e"}, - {file = "typer-0.6.1.tar.gz", hash = "sha256:2d5720a5e63f73eaf31edaa15f6ab87f35f0690f8ca233017d7d23d743a91d73"}, + {file = "typer-0.16.1-py3-none-any.whl", hash = "sha256:90ee01cb02d9b8395ae21ee3368421faf21fa138cb2a541ed369c08cec5237c9"}, + {file = "typer-0.16.1.tar.gz", hash = "sha256:d358c65a464a7a90f338e3bb7ff0c74ac081449e53884b12ba658cbd72990614"}, ] [[package]] @@ -921,16 +885,6 @@ files = [ {file = "types_certifi-2020.4.0-py3-none-any.whl", hash = "sha256:0ffdbe451d3b02f6d2cfd87bcfb2f086a4ff1fa76a35d51cfc3771e261d7a8fd"}, ] -[[package]] -name = "types-python-dateutil" -version = "2.8.0" -summary = "Typing stubs for python-dateutil" -groups = ["dev"] -files = [ - {file = "types-python-dateutil-2.8.0.tar.gz", hash = "sha256:540c6c53c3a52433d7088254e3afdc3f6c86b5ae452aaa1b796c26d01c9fd73c"}, - {file = "types_python_dateutil-2.8.0-py3-none-any.whl", hash = "sha256:9954d87dc982344bb2aad73a7fe505bdca72f89088ef653c4c40f52649183437"}, -] - [[package]] name = "types-pyyaml" version = "6.0.3" @@ -943,11 +897,11 @@ files = [ [[package]] name = "typing-extensions" -version = "4.8.0" -requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.16.0" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" groups = ["default", "dev"] files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] diff --git a/pyproject.toml b/pyproject.toml index 4966e5f55..f0245dbf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,23 +2,21 @@ authors = [ { name = "Dylan Anthony", email = "contact@dylananthony.com" }, ] -license = { text = "MIT" } -requires-python = ">=3.9,<4.0" +license = "MIT" +requires-python = ">=3.11,<4.0" dependencies = [ "jinja2>=3.0.0,<4.0.0", - "typer>0.6,<0.16", + "typer>0.16,<0.28", "colorama>=0.4.3; sys_platform == \"win32\"", "shellingham>=1.3.2,<2.0.0", - "pydantic>=2.1.1,<3.0.0", + "pydantic>=2.10,<3.0.0", "attrs>=22.2.0", - "python-dateutil>=2.8.1,<3.0.0", - "httpx>=0.20.0,<0.29.0", - "ruamel.yaml>=0.18.6,<0.19.0", - "ruff>=0.2,<0.9", - "typing-extensions>=4.8.0,<5.0.0", + "httpx2>=2.13.1,<3", + "ruamel.yaml>=0.18.6,<0.20.0", + "ruff>=0.2", ] name = "openapi-python-client" -version = "0.23.0" +version = "0.30.0" description = "Generate modern Python clients from OpenAPI" keywords = [ "OpenAPI", @@ -27,14 +25,12 @@ keywords = [ ] classifiers = [ "Development Status :: 4 - Beta", - "License :: OSI Approved :: MIT License", "Intended Audience :: Developers", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "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", "Topic :: Software Development :: Code Generators", "Typing :: Typed", ] @@ -46,6 +42,20 @@ repository = "https://github.com/openapi-generators/openapi-python-client" [project.scripts] openapi-python-client = "openapi_python_client.cli:app" +[dependency-groups] +dev = [ + "mypy>=1.13", + "pytest>8", + "pytest-cov", + "pytest-mock>3", + "python-multipart", + "ruff>=0.2,<0.18", + "syrupy>=4", + "hypothesis>=6.100", + "types-PyYAML<7.0.0,>=6.0.3", + "types-certifi<2021.10.9,>=2020.0.0", +] + [tool.ruff] line-length = 120 exclude = [ @@ -53,7 +63,8 @@ exclude = [ ".mypy_cache", ".venv", "openapi_python_client/templates/*", - "end_to_end_tests/*", + "end_to_end_tests/golden-records/*", + "end_to_end_tests/__snapshots__/*", "tests/test_templates/*", ] @@ -63,10 +74,10 @@ ignore = ["E501", "PLR0913", "PLR2004"] [tool.ruff.lint.per-file-ignores] "openapi_python_client/cli.py" = ["B008"] -"tests/*" = ["PLR2004"] +"tests/*" = ["PLR2004", "PLR0917"] [tool.coverage.run] -omit = ["openapi_python_client/__main__.py", "openapi_python_client/templates/*"] +omit = ["openapi_python_client/__main__.py", "openapi_python_client/templates/*", "end_to_end_tests/*", "integration_tests/*", "tests/*"] [tool.mypy] plugins = ["pydantic.mypy"] @@ -85,19 +96,8 @@ ignore_missing_imports = true [tool.pytest.ini_options] junit_family = "xunit2" -[tool.pdm.dev-dependencies] -dev = [ - "pytest>8", - "pytest-mock>3", - "mypy>=1.13", - "pytest-cov", - "python-multipart", - "types-PyYAML<7.0.0,>=6.0.3", - "types-certifi<2021.10.9,>=2020.0.0", - "types-python-dateutil<3.0.0,>=2.0.0", - "ruamel-yaml-string>=0.1.1", - "syrupy>=4", -] +[tool.pdm.resolution] +exclude-newer = "7d" [tool.pdm.build] includes = [ @@ -107,18 +107,19 @@ includes = [ ] [tool.pdm.scripts] -lint = "ruff check --fix ." +lint = "ruff check ." format = "ruff format ." -mypy = "mypy openapi_python_client" +mypy = "mypy openapi_python_client fuzz" check = { composite = ["lint", "format", "mypy", "test"] } -regen = {composite = ["regen_e2e", "regen_integration"]} +regen = { composite = ["regen_e2e", "regen_integration"] } e2e = "pytest openapi_python_client end_to_end_tests/test_end_to_end.py" -re = {composite = ["regen_e2e", "e2e --snapshot-update"]} +re = { composite = ["regen_e2e", "e2e --snapshot-update"] } regen_e2e = "python -m end_to_end_tests.regen_golden_record" unit_test = "pytest tests" +fuzz = "pytest fuzz --hypothesis-show-statistics" [tool.pdm.scripts.test] -cmd = "pytest tests end_to_end_tests/test_end_to_end.py --basetemp=tests/tmp" +cmd = "pytest tests end_to_end_tests/test_end_to_end.py end_to_end_tests/functional_tests --basetemp=tests/tmp" [tool.pdm.scripts.test.env] "TEST_RELATIVE" = "true" @@ -130,7 +131,7 @@ composite = ["test --cov openapi_python_client tests --cov-report=term-missing"] [tool.pdm.scripts.regen_integration] shell = """ -openapi-python-client generate --overwrite --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.json --config integration-tests/config.yaml --meta pdm --output-path integration-tests \ +openapi-python-client generate --overwrite --url https://raw.githubusercontent.com/openapi-generators/openapi-test-server/refs/tags/v0.2.1/openapi.yaml --config integration-tests/config.yaml --meta none --output-path integration-tests/integration_tests \ """ [build-system] diff --git a/tests/conftest.py b/tests/conftest.py index 969e57cbd..c67d076d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,13 @@ from __future__ import annotations +from collections.abc import Callable from pathlib import Path -from typing import Any, Callable +from typing import Any import pytest from mypy.semanal_shared import Protocol -from openapi_python_client import Config, MetaType +from openapi_python_client import Config, MetaType, strings from openapi_python_client import schema as oai from openapi_python_client.config import ConfigFile from openapi_python_client.parser.properties import ( @@ -29,15 +30,16 @@ from openapi_python_client.parser.properties.protocol import PropertyType, Value from openapi_python_client.schema.openapi_schema_pydantic import Parameter from openapi_python_client.schema.parameter_location import ParameterLocation -from openapi_python_client.utils import ClassName, PythonIdentifier +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import ClassName, PythonIdentifier @pytest.fixture(scope="session") def config() -> Config: """Create a default config for when it doesn't matter""" return Config.from_sources( - ConfigFile(), - MetaType.POETRY, + config_file=ConfigFile(), + meta_type=MetaType.POETRY, document_source=Path("openapi.yaml"), file_encoding="utf-8", overwrite=False, @@ -56,13 +58,14 @@ def model_property_factory() -> ModelFactory: You can pass the same params into this as the ModelProperty constructor to override defaults. """ - from openapi_python_client.parser.properties import Class def _factory(**kwargs): kwargs = _common_kwargs(kwargs) kwargs = { "description": "", - "class_info": Class(name=ClassName("MyClass", ""), module_name=PythonIdentifier("my_module", "")), + "class_info": Class( + name=ClassName(UntrustedString("MyClass"), ""), module_name=PythonIdentifier("my_module", "") + ), "data": oai.Schema.model_construct(), "roots": set(), "required_properties": None, @@ -130,12 +133,11 @@ def enum_property_factory() -> EnumFactory[EnumProperty]: You can pass the same params into this as the EnumProerty constructor to override defaults. """ - from openapi_python_client.parser.properties import Class return _simple_factory( EnumProperty, lambda kwargs: { - "class_info": Class(name=kwargs["name"], module_name=kwargs["name"]), + "class_info": Class(name=ClassName(kwargs["name"], prefix=""), module_name=kwargs["name"]), "values": {}, "value_type": str, }, @@ -149,12 +151,11 @@ def literal_enum_property_factory() -> EnumFactory[LiteralEnumProperty]: You can pass the same params into this as the LiteralEnumProerty constructor to override defaults. """ - from openapi_python_client.parser.properties import Class return _simple_factory( LiteralEnumProperty, lambda kwargs: { - "class_info": Class(name=kwargs["name"], module_name=kwargs["name"]), + "class_info": Class(name=UntrustedString(kwargs["name"]), module_name=kwargs["name"]), "values": set(), "value_type": str, }, @@ -323,6 +324,7 @@ def _common_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]: "example": None, **kwargs, } + kwargs["name"] = UntrustedString(kwargs["name"]) if not kwargs.get("python_name"): - kwargs["python_name"] = kwargs["name"] + kwargs["python_name"] = strings.PythonIdentifier(value=kwargs["name"], prefix="") return kwargs diff --git a/tests/test___init__.py b/tests/test___init__.py index c419fdf32..831ba815b 100644 --- a/tests/test___init__.py +++ b/tests/test___init__.py @@ -1,17 +1,27 @@ +from pathlib import Path + import pytest -from openapi_python_client import Config, ErrorLevel, Project +from openapi_python_client import Config, ErrorLevel, GeneratorData, MetaType, Project from openapi_python_client.config import ConfigFile +from openapi_python_client.schema.untrusted_string import UntrustedString default_http_timeout = ConfigFile.model_json_schema()["properties"]["http_timeout"]["default"] def make_project(config: Config) -> Project: - from unittest.mock import MagicMock - - from openapi_python_client import Project - - return Project(openapi=MagicMock(title="My Test API"), config=config) + return Project( + openapi=GeneratorData( + title=UntrustedString("My API Client"), + description=None, + models=[], + version="", + errors=[], + endpoint_collections_by_tag={}, + enums=[], + ), + config=config, + ) @pytest.fixture @@ -29,9 +39,6 @@ class TestProject: def test__run_post_hooks_reports_missing_commands(self, project_with_dir: Project) -> None: fake_command_name = "blahblahdoesntexist" project_with_dir.config.post_hooks = [fake_command_name] - need_to_make_cwd = not project_with_dir.project_dir.exists() - if need_to_make_cwd: - project_with_dir.project_dir.mkdir() project_with_dir._run_post_hooks() @@ -42,23 +49,42 @@ def test__run_post_hooks_reports_missing_commands(self, project_with_dir: Projec assert fake_command_name in error.detail def test__run_post_hooks_reports_stdout_of_commands_that_error_with_no_stderr(self, project_with_dir): - failing_command = "python -c \"print('a message'); exit(1)\"" + failing_command = "python3 -c \"print('a message'); exit(1)\"" project_with_dir.config.post_hooks = [failing_command] project_with_dir._run_post_hooks() assert len(project_with_dir.errors) == 1 error = project_with_dir.errors[0] assert error.level == ErrorLevel.ERROR - assert error.header == "python failed" + assert error.header == "python3 failed" assert "a message" in error.detail def test__run_post_hooks_reports_stderr_of_commands_that_error(self, project_with_dir): - failing_command = "python -c \"print('a message'); raise Exception('some exception')\"" + failing_command = "python3 -c \"print('a message'); raise Exception('some exception')\"" project_with_dir.config.post_hooks = [failing_command] project_with_dir._run_post_hooks() assert len(project_with_dir.errors) == 1 error = project_with_dir.errors[0] assert error.level == ErrorLevel.ERROR - assert error.header == "python failed" + assert error.header == "python3 failed" assert "some exception" in error.detail + + def test_build_creates_parent_directories(self, tmp_path): + fresh_config = Config.from_sources( + config_file=ConfigFile(), + meta_type=MetaType.POETRY, + document_source=Path("openapi.yaml"), + file_encoding="utf-8", + overwrite=False, + output_path=None, + ) + project = make_project(fresh_config) + project.project_dir = tmp_path / "nested" / "dir" / "my-api-client" + project.package_dir = project.project_dir / project.package_name + + errors = project.build() + + assert errors == [] + assert project.project_dir.exists() + assert project.project_dir.is_dir() diff --git a/tests/test_cli.py b/tests/test_cli.py index 8679584fd..9712b2a04 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,11 +1,11 @@ from typer.testing import CliRunner +from openapi_python_client.cli import app + runner = CliRunner() def test_version() -> None: - from openapi_python_client.cli import app - result = runner.invoke(app, ["--version", "generate"]) assert result.exit_code == 0 @@ -13,28 +13,22 @@ def test_version() -> None: def test_bad_config() -> None: - from openapi_python_client.cli import app - config_path = "config/path" path = "cool/path" result = runner.invoke(app, ["generate", f"--config={config_path}", f"--path={path}"]) assert result.exit_code == 2 - assert "Unable to parse config" in result.stdout + assert "Unable to parse config" in result.output class TestGenerate: def test_generate_no_params(self) -> None: - from openapi_python_client.cli import app - result = runner.invoke(app, ["generate"]) assert result.exit_code == 1, result.output def test_generate_url_and_path(self) -> None: - from openapi_python_client.cli import app - result = runner.invoke(app, ["generate", "--path=blah", "--url=otherblah"]) assert result.exit_code == 1 @@ -43,8 +37,6 @@ def test_generate_url_and_path(self) -> None: def test_generate_encoding_errors(self) -> None: path = "cool/path" file_encoding = "error-file-encoding" - from openapi_python_client.cli import app - result = runner.invoke(app, ["generate", f"--path={path}", f"--file-encoding={file_encoding}"]) assert result.exit_code == 1 diff --git a/tests/test_config.py b/tests/test_config.py index c24766f9b..36cd55ca0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,11 +1,21 @@ import json import os +from io import StringIO from pathlib import Path +from typing import Any import pytest -from ruamel.yaml import YAML +from ruamel.yaml import YAML as _YAML + +from openapi_python_client.config import ClassOverride, ConfigFile + + +class YAML(_YAML): + def dump_to_string(self, data: Any, **kwargs: Any) -> str: + stream = StringIO() + self.dump(data=data, stream=stream, **kwargs) + return stream.getvalue() -from openapi_python_client.config import ConfigFile yaml = YAML(typ=["safe", "string"]) @@ -44,8 +54,8 @@ def test_load_from_path(tmp_path: Path, filename, dump, relative) -> None: config = ConfigFile.load_from_path(yml_file) assert config.field_prefix == "blah" - assert config.class_overrides["Class1"].model_dump() == override1 - assert config.class_overrides["Class2"].model_dump() == override2 + assert config.class_overrides["Class1"] == ClassOverride.model_validate(override1) + assert config.class_overrides["Class2"] == ClassOverride.model_validate(override2) assert config.project_name_override == "project-name" assert config.package_name_override == "package_name" assert config.package_version_override == "package_version" diff --git a/tests/test_parser/test_bodies.py b/tests/test_parser/test_bodies.py index 0956d11f6..a18eddb04 100644 --- a/tests/test_parser/test_bodies.py +++ b/tests/test_parser/test_bodies.py @@ -1,7 +1,8 @@ from openapi_python_client import schema as oai -from openapi_python_client.parser.bodies import body_from_data +from openapi_python_client.parser.bodies import Body, body_from_data from openapi_python_client.parser.errors import ParseError from openapi_python_client.parser.properties import Schemas +from openapi_python_client.schema import UntrustedString def test_errors(config): @@ -38,3 +39,27 @@ def test_errors(config): assert len(errs) == len(operation.request_body.content) assert all(isinstance(err, ParseError) for err in errs) + + +def test_content_type_with_special_characters(config): + """Content types containing quotes or other special characters must stay wrapped for safe template rendering""" + operation = oai.Operation( + requestBody=oai.RequestBody( + content={ + 'application/json; profile="https://example.com"': oai.MediaType( + media_type_schema=oai.Schema(type=oai.DataType.STRING) + ) + } + ), + responses={}, + ) + + bodies, _ = body_from_data( + data=operation, schemas=Schemas(), config=config, endpoint_name="endpoint", request_bodies={} + ) + + assert len(bodies) == 1 + body = bodies[0] + assert isinstance(body, Body) + assert isinstance(body.content_type, UntrustedString) + assert body.content_type == 'application/json; profile="https://example.com"' diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index 57a07070b..535a04ffa 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -4,136 +4,31 @@ import pytest import openapi_python_client.schema as oai -from openapi_python_client import GeneratorError from openapi_python_client.parser.errors import ParseError -from openapi_python_client.parser.openapi import Endpoint, EndpointCollection -from openapi_python_client.parser.properties import IntProperty, Parameters, Schemas +from openapi_python_client.parser.openapi import Endpoint, EndpointCollection, import_string_from_class +from openapi_python_client.parser.properties import Class, IntProperty, Parameters, Schemas from openapi_python_client.schema import DataType +from openapi_python_client.schema.ref import Ref +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import PythonCode, PythonIdentifier MODULE_NAME = "openapi_python_client.parser.openapi" -class TestGeneratorData: - def test_from_dict_invalid_schema(self, mocker): - Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") - config = mocker.MagicMock() - - in_dict = {} - - from openapi_python_client.parser.openapi import GeneratorData - - generator_data = GeneratorData.from_dict(in_dict, config=config) - - assert isinstance(generator_data, GeneratorError) - assert generator_data.header == "Failed to parse OpenAPI document" - keywords = ["3 validation errors for OpenAPI", "info", "paths", "openapi", "Field required"] - assert generator_data.detail and all(keyword in generator_data.detail for keyword in keywords) - - Schemas.build.assert_not_called() - Schemas.assert_not_called() - - def test_swagger_document_invalid_schema(self, mocker): - Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") - config = mocker.MagicMock() - - in_dict = {"swagger": "2.0"} - - from openapi_python_client.parser.openapi import GeneratorData - - generator_data = GeneratorData.from_dict(in_dict, config=config) - - assert isinstance(generator_data, GeneratorError) - assert generator_data.header == "Failed to parse OpenAPI document" - keywords = [ - "You may be trying to use a Swagger document; this is not supported by this project.", - "info", - "paths", - "openapi", - "Field required", - ] - assert generator_data.detail and all(keyword in generator_data.detail for keyword in keywords) - - Schemas.build.assert_not_called() - Schemas.assert_not_called() - - class TestEndpoint: def make_endpoint(self): - from openapi_python_client.parser.openapi import Endpoint - return Endpoint( - path="path", + path=UntrustedString("path"), method="method", - description=None, - name="name", + description=UntrustedString(""), + name=PythonIdentifier("name", prefix=""), requires_security=False, - tags=["tag"], + tags=[PythonIdentifier("tag", prefix="")], relative_imports={"import_3"}, + summary=UntrustedString("summary"), ) - @pytest.mark.parametrize("response_status_code", ["not_a_number", 499]) - def test__add_responses_status_code_error(self, response_status_code, mocker): - from openapi_python_client.parser.openapi import Endpoint, Schemas - - schemas = Schemas() - response_1_data = mocker.MagicMock() - data = { - response_status_code: response_1_data, - } - endpoint = self.make_endpoint() - parse_error = ParseError(data=mocker.MagicMock()) - response_from_data = mocker.patch(f"{MODULE_NAME}.response_from_data", return_value=(parse_error, schemas)) - config = MagicMock() - - response, schemas = Endpoint._add_responses(endpoint=endpoint, data=data, schemas=schemas, config=config) - - assert response.errors == [ - ParseError( - detail=f"Invalid response status code {response_status_code} (not a valid HTTP status code), " - "response will be omitted from generated client" - ) - ] - response_from_data.assert_not_called() - - def test__add_responses_error(self, mocker): - from openapi_python_client.parser.openapi import Endpoint, Schemas - - schemas = Schemas() - response_1_data = mocker.MagicMock() - response_2_data = mocker.MagicMock() - data = { - "200": response_1_data, - "404": response_2_data, - } - endpoint = self.make_endpoint() - parse_error = ParseError(data=mocker.MagicMock(), detail="some problem") - response_from_data = mocker.patch(f"{MODULE_NAME}.response_from_data", return_value=(parse_error, schemas)) - config = MagicMock() - - response, schemas = Endpoint._add_responses(endpoint=endpoint, data=data, schemas=schemas, config=config) - - response_from_data.assert_has_calls( - [ - mocker.call(status_code=200, data=response_1_data, schemas=schemas, parent_name="name", config=config), - mocker.call(status_code=404, data=response_2_data, schemas=schemas, parent_name="name", config=config), - ] - ) - assert response.errors == [ - ParseError( - detail="Cannot parse response for status code 200 (some problem), " - "response will be omitted from generated client", - data=parse_error.data, - ), - ParseError( - detail="Cannot parse response for status code 404 (some problem), " - "response will be omitted from generated client", - data=parse_error.data, - ), - ] - def test_add_parameters_handles_no_params(self): - from openapi_python_client.parser.openapi import Endpoint, Schemas - endpoint = self.make_endpoint() schemas = Schemas() parameters = Parameters() @@ -148,36 +43,6 @@ def test_add_parameters_handles_no_params(self): config=config, ) == (endpoint, schemas, parameters) - def test_add_parameters_parse_error(self, mocker): - from openapi_python_client.parser.openapi import Endpoint - - endpoint = self.make_endpoint() - initial_schemas = mocker.MagicMock() - initial_parameters = mocker.MagicMock() - parse_error = ParseError(data=mocker.MagicMock()) - property_schemas = mocker.MagicMock() - mocker.patch(f"{MODULE_NAME}.property_from_data", return_value=(parse_error, property_schemas)) - param = oai.Parameter.model_construct( - name="test", required=True, param_schema=mocker.MagicMock(), param_in="cookie" - ) - config = MagicMock() - - result, schemas, parameters = Endpoint.add_parameters( - endpoint=endpoint, - data=oai.Operation.model_construct(parameters=[param]), - schemas=initial_schemas, - parameters=initial_parameters, - config=config, - ) - assert (result, schemas, parameters) == ( - ParseError( - data=parse_error.data, - detail=f"cannot parse parameter of endpoint {endpoint.name}: {parse_error.detail}", - ), - initial_schemas, - initial_parameters, - ) - @pytest.mark.parametrize( "data_type, allowed", [ @@ -190,13 +55,14 @@ def test_add_parameters_parse_error(self, mocker): ], ) def test_add_parameters_header_types(self, data_type, allowed, config): - from openapi_python_client.parser.openapi import Endpoint - endpoint = self.make_endpoint() initial_schemas = Schemas() parameters = Parameters() param = oai.Parameter.model_construct( - name="test", required=True, param_schema=oai.Schema(type=data_type), param_in=oai.ParameterLocation.HEADER + name=UntrustedString("test"), + required=True, + param_schema=oai.Schema(type=data_type), + param_in=oai.ParameterLocation.HEADER, ) result = Endpoint.add_parameters( @@ -214,7 +80,7 @@ def test_add_parameters_header_types(self, data_type, allowed, config): def test__add_parameters_parse_error_on_non_required_path_param(self, config): endpoint = self.make_endpoint() param = oai.Parameter.model_construct( - name="test", + name=UntrustedString("test"), required=False, param_schema=oai.Schema.model_construct(type="string"), param_in=oai.ParameterLocation.PATH, @@ -242,7 +108,7 @@ def test__add_parameters_handles_invalid_references(self, config): endpoint = self.make_endpoint() data = oai.Operation.model_construct( parameters=[ - oai.Reference.model_construct(ref="blah"), + oai.Reference.model_construct(ref=Ref("blah")), ] ) @@ -254,29 +120,6 @@ def test__add_parameters_handles_invalid_references(self, config): assert isinstance(error, ParseError) assert parameters == return_parameters - def test__add_parameters_resolves_references(self, mocker, param_factory, config): - """References are not supported as direct params yet""" - endpoint = self.make_endpoint() - data = oai.Operation.model_construct( - parameters=[ - oai.Reference.model_construct(ref="#components/parameters/blah"), - ] - ) - - parameters = mocker.MagicMock() - new_param = param_factory(name="blah", schema=oai.Schema.model_construct(type="string")) - parameters.classes_by_name = { - "blah": new_param, - } - parameters.classes_by_reference = {"components/parameters/blah": new_param} - - (endpoint, _, return_parameters) = endpoint.add_parameters( - endpoint=endpoint, data=data, schemas=Schemas(), parameters=parameters, config=config - ) - - assert isinstance(endpoint, Endpoint) - assert parameters == return_parameters - def test__add_parameters_skips_params_without_schemas(self, config): """Params without schemas are allowed per spec, but the any type doesn't make sense as a parameter""" endpoint = self.make_endpoint() @@ -301,19 +144,19 @@ def test__add_parameters_same_identifier_conflict(self, config): data = oai.Operation.model_construct( parameters=[ oai.Parameter.model_construct( - name="param", + name=UntrustedString("param"), param_in="path", param_schema=oai.Schema.model_construct(type="string"), required=True, ), oai.Parameter.model_construct( - name="param_path", + name=UntrustedString("param_path"), param_in="path", param_schema=oai.Schema.model_construct(type="string"), required=True, ), oai.Parameter.model_construct( - name="param", + name=UntrustedString("param"), param_in="query", param_schema=oai.Schema.model_construct(type="string"), ), @@ -332,13 +175,13 @@ def test__add_parameters_query_optionality(self, config): data = oai.Operation.model_construct( parameters=[ oai.Parameter.model_construct( - name="not_required", + name=UntrustedString("not_required"), required=False, param_schema=oai.Schema.model_construct(type="string"), param_in="query", ), oai.Parameter.model_construct( - name="required", + name=UntrustedString("required"), required=True, param_schema=oai.Schema.model_construct(type="string"), param_in="query", @@ -358,11 +201,12 @@ def test__add_parameters_query_optionality(self, config): assert not param.required def test_add_parameters_duplicate_properties(self, config): - from openapi_python_client.parser.openapi import Endpoint, Schemas - endpoint = self.make_endpoint() param = oai.Parameter.model_construct( - name="test", required=True, param_schema=oai.Schema.model_construct(type="string"), param_in="path" + name=UntrustedString("test"), + required=True, + param_schema=oai.Schema.model_construct(type="string"), + param_in="path", ) data = oai.Operation.model_construct(parameters=[param, param]) schemas = Schemas() @@ -383,14 +227,18 @@ def test_add_parameters_duplicate_properties(self, config): ) def test_add_parameters_duplicate_properties_different_location(self, config): - from openapi_python_client.parser.openapi import Endpoint, Schemas - endpoint = self.make_endpoint() path_param = oai.Parameter.model_construct( - name="test", required=True, param_schema=oai.Schema.model_construct(type="string"), param_in="path" + name=UntrustedString("test"), + required=True, + param_schema=oai.Schema.model_construct(type="string"), + param_in="path", ) query_param = oai.Parameter.model_construct( - name="test", required=True, param_schema=oai.Schema.model_construct(type="string"), param_in="query" + name=UntrustedString("test"), + required=True, + param_schema=oai.Schema.model_construct(type="string"), + param_in="query", ) schemas = Schemas() parameters = Parameters() @@ -407,10 +255,8 @@ def test_add_parameters_duplicate_properties_different_location(self, config): assert result.query_parameters[0].name == "test" def test_sort_parameters(self, string_property_factory): - from openapi_python_client.parser.openapi import Endpoint - endpoint = self.make_endpoint() - endpoint.path = "/multiple-path-parameters/{param4}/{param2}/{param1}/{param3}" + endpoint.path = UntrustedString("/multiple-path-parameters/{param4}/{param2}/{param1}/{param3}") for i in range(1, 5): prop = string_property_factory(name=f"param{i}") @@ -423,10 +269,8 @@ def test_sort_parameters(self, string_property_factory): assert result_names == expected_names def test_sort_parameters_missing_param(self, string_property_factory): - from openapi_python_client.parser.openapi import Endpoint - endpoint = self.make_endpoint() - endpoint.path = "/multiple-path-parameters/{param1}/{param2}" + endpoint.path = UntrustedString("/multiple-path-parameters/{param1}/{param2}") param = string_property_factory(name="param1") endpoint.path_parameters.append(param) @@ -434,13 +278,11 @@ def test_sort_parameters_missing_param(self, string_property_factory): assert isinstance(result, ParseError) assert "Incorrect path templating" in result.detail - assert endpoint.path in result.detail + assert endpoint.path.get_untrusted_value() in result.detail def test_sort_parameters_extra_param(self, string_property_factory): - from openapi_python_client.parser.openapi import Endpoint - endpoint = self.make_endpoint() - endpoint.path = "/multiple-path-parameters" + endpoint.path = UntrustedString("/multiple-path-parameters") param = string_property_factory(name="param1") endpoint.path_parameters.append(param) @@ -448,238 +290,7 @@ def test_sort_parameters_extra_param(self, string_property_factory): assert isinstance(result, ParseError) assert "Incorrect path templating" in result.detail - assert endpoint.path in result.detail - - def test_from_data_bad_params(self, mocker, config): - from openapi_python_client.parser.openapi import Endpoint - - path = mocker.MagicMock() - method = mocker.MagicMock() - parse_error = ParseError(data=mocker.MagicMock()) - return_schemas = mocker.MagicMock() - return_parameters = mocker.MagicMock() - mocker.patch.object(Endpoint, "add_parameters", return_value=(parse_error, return_schemas, return_parameters)) - data = oai.Operation.model_construct( - description=mocker.MagicMock(), - operationId=mocker.MagicMock(), - security={"blah": "bloo"}, - responses=mocker.MagicMock(), - ) - initial_schemas = mocker.MagicMock() - parameters = Parameters() - - result = Endpoint.from_data( - data=data, - path=path, - method=method, - tags=["default"], - schemas=initial_schemas, - parameters=parameters, - config=config, - request_bodies={}, - ) - - assert result == (parse_error, return_schemas, return_parameters) - - def test_from_data_bad_responses(self, mocker, config): - from openapi_python_client.parser.openapi import Endpoint - - path = mocker.MagicMock() - method = mocker.MagicMock() - parse_error = ParseError(data=mocker.MagicMock()) - param_schemas = mocker.MagicMock() - return_parameters = mocker.MagicMock() - mocker.patch.object( - Endpoint, "add_parameters", return_value=(mocker.MagicMock(), param_schemas, return_parameters) - ) - response_schemas = mocker.MagicMock() - _add_responses = mocker.patch.object(Endpoint, "_add_responses", return_value=(parse_error, response_schemas)) - data = oai.Operation.model_construct( - description=mocker.MagicMock(), - operationId=mocker.MagicMock(), - security={"blah": "bloo"}, - responses=mocker.MagicMock(), - ) - initial_schemas = mocker.MagicMock() - initial_parameters = mocker.MagicMock() - - result = Endpoint.from_data( - data=data, - path=path, - method=method, - tags=["default"], - schemas=initial_schemas, - parameters=initial_parameters, - config=config, - request_bodies={}, - ) - - assert result == (parse_error, response_schemas, return_parameters) - - def test_from_data_standard(self, mocker, config): - from openapi_python_client.parser.openapi import Endpoint - - path = mocker.MagicMock() - method = mocker.MagicMock() - param_schemas = mocker.MagicMock() - param_endpoint = mocker.MagicMock() - return_parameters = mocker.MagicMock() - add_parameters = mocker.patch.object( - Endpoint, "add_parameters", return_value=(param_endpoint, param_schemas, return_parameters) - ) - response_schemas = mocker.MagicMock() - response_endpoint = mocker.MagicMock() - _add_responses = mocker.patch.object( - Endpoint, "_add_responses", return_value=(response_endpoint, response_schemas) - ) - data = oai.Operation.model_construct( - description=mocker.MagicMock(), - operationId=mocker.MagicMock(), - security={"blah": "bloo"}, - responses=mocker.MagicMock(), - ) - initial_schemas = mocker.MagicMock() - initial_parameters = mocker.MagicMock() - - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) - - Endpoint.from_data( - data=data, - path=path, - method=method, - tags=["default"], - schemas=initial_schemas, - parameters=initial_parameters, - config=config, - request_bodies={}, - ) - - add_parameters.assert_called_once_with( - endpoint=Endpoint( - path=path, - method=method, - description=data.description, - summary="", - name=data.operationId, - requires_security=True, - tags=["default"], - ), - data=data, - schemas=initial_schemas, - parameters=initial_parameters, - config=config, - ) - _add_responses.assert_called_once_with( - endpoint=param_endpoint, data=data.responses, schemas=param_schemas, config=config - ) - - def test_from_data_no_operation_id(self, mocker, config): - from openapi_python_client.parser.openapi import Endpoint - - path = "/path/with/{param}/" - method = "get" - add_parameters = mocker.patch.object( - Endpoint, "add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock(), mocker.MagicMock()) - ) - _add_responses = mocker.patch.object( - Endpoint, "_add_responses", return_value=(mocker.MagicMock(), mocker.MagicMock()) - ) - data = oai.Operation.model_construct( - description=mocker.MagicMock(), - operationId=None, - security={"blah": "bloo"}, - responses=mocker.MagicMock(), - ) - schemas = mocker.MagicMock() - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) - parameters = mocker.MagicMock() - - endpoint, _, return_params = Endpoint.from_data( - data=data, - path=path, - method=method, - tags=["default"], - schemas=schemas, - parameters=parameters, - config=config, - request_bodies={}, - ) - - add_parameters.assert_called_once_with( - endpoint=Endpoint( - path=path, - method=method, - description=data.description, - summary="", - name="get_path_with_param", - requires_security=True, - tags=["default"], - ), - data=data, - schemas=schemas, - config=config, - parameters=parameters, - ) - _add_responses.assert_called_once_with( - endpoint=add_parameters.return_value[0], - data=data.responses, - schemas=add_parameters.return_value[1], - config=config, - ) - - def test_from_data_no_security(self, mocker, config): - from openapi_python_client.parser.openapi import Endpoint - - data = oai.Operation.model_construct( - description=mocker.MagicMock(), - operationId=mocker.MagicMock(), - security=None, - responses=mocker.MagicMock(), - ) - add_parameters = mocker.patch.object( - Endpoint, "add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock(), mocker.MagicMock()) - ) - _add_responses = mocker.patch.object( - Endpoint, "_add_responses", return_value=(mocker.MagicMock(), mocker.MagicMock()) - ) - path = mocker.MagicMock() - method = mocker.MagicMock() - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) - schemas = mocker.MagicMock() - parameters = mocker.MagicMock() - - Endpoint.from_data( - data=data, - path=path, - method=method, - tags=["a"], - schemas=schemas, - parameters=parameters, - config=config, - request_bodies={}, - ) - - add_parameters.assert_called_once_with( - endpoint=Endpoint( - path=path, - method=method, - description=data.description, - summary="", - name=data.operationId, - requires_security=False, - tags=["a"], - ), - data=data, - parameters=parameters, - schemas=schemas, - config=config, - ) - _add_responses.assert_called_once_with( - endpoint=add_parameters.return_value[0], - data=data.responses, - schemas=add_parameters.return_value[1], - config=config, - ) + assert endpoint.path.get_untrusted_value() in result.detail def test_from_data_some_bad_bodies(self, config): endpoint, _, _ = Endpoint.from_data( @@ -693,10 +304,11 @@ def test_from_data_some_bad_bodies(self, config): ), ), schemas=Schemas(), + responses={}, config=config, parameters=Parameters(), tags=["tag"], - path="/", + path=UntrustedString("/"), method="get", request_bodies={}, ) @@ -716,10 +328,11 @@ def test_from_data_all_bodies_bad(self, config): ), ), schemas=Schemas(), + responses={}, config=config, parameters=Parameters(), tags=["tag"], - path="/", + path=UntrustedString("/"), method="get", request_bodies={}, ) @@ -728,32 +341,26 @@ def test_from_data_all_bodies_bad(self, config): @pytest.mark.parametrize( "response_types, expected", - (([], "Any"), (["Something"], "Something"), (["First", "Second", "Second"], "Union[First, Second]")), + (([], "Any"), (["Something"], "Something"), (["First", "Second", "Second"], "First | Second")), ) def test_response_type(self, response_types, expected): endpoint = self.make_endpoint() for response_type in response_types: mock_response = MagicMock() - mock_response.prop.get_type_string.return_value = response_type - endpoint.responses.append(mock_response) + mock_response.prop.get_type_string.return_value = PythonCode(response_type) + endpoint.responses.patterns.append(mock_response) - assert endpoint.response_type() == expected + assert endpoint.response_type() == PythonCode(expected) class TestImportStringFromReference: def test_import_string_from_reference_no_prefix(self, mocker): - from openapi_python_client.parser.openapi import import_string_from_class - from openapi_python_client.parser.properties import Class - class_ = mocker.MagicMock(autospec=Class) result = import_string_from_class(class_) assert result == f"from .{class_.module_name} import {class_.name}" def test_import_string_from_reference_with_prefix(self, mocker): - from openapi_python_client.parser.openapi import import_string_from_class - from openapi_python_client.parser.properties import Class - prefix = mocker.MagicMock(autospec=str) class_ = mocker.MagicMock(autospec=Class) result = import_string_from_class(class_=class_, prefix=prefix) @@ -764,29 +371,34 @@ def test_import_string_from_reference_with_prefix(self, mocker): class TestEndpointCollection: def test_from_data_overrides_path_item_params_with_operation_params(self, config): data = { - "/": oai.PathItem.model_construct( + UntrustedString("/"): oai.PathItem.model_construct( parameters=[ oai.Parameter.model_construct( - name="param", param_in="query", param_schema=oai.Schema.model_construct(type="string") + name=UntrustedString("param"), + param_in="query", + param_schema=oai.Schema.model_construct(type="string"), ), ], get=oai.Operation.model_construct( parameters=[ oai.Parameter.model_construct( - name="param", param_in="query", param_schema=oai.Schema.model_construct(type="integer") + name=UntrustedString("param"), + param_in="query", + param_schema=oai.Schema.model_construct(type="integer"), ) ], - responses={"200": oai.Response.model_construct(description="blah")}, + responses={UntrustedString("200"): oai.Response.model_construct(description="blah")}, ), ) } - collections, schemas, parameters = EndpointCollection.from_data( + collections, _schemas, _parameters = EndpointCollection.from_data( data=data, schemas=Schemas(), parameters=Parameters(), config=config, request_bodies={}, + responses={}, ) collection: EndpointCollection = collections["default"] assert isinstance(collection.endpoints[0].query_parameters[0], IntProperty) diff --git a/tests/test_parser/test_properties/test_any.py b/tests/test_parser/test_properties/test_any.py deleted file mode 100644 index d80e93e64..000000000 --- a/tests/test_parser/test_properties/test_any.py +++ /dev/null @@ -1,13 +0,0 @@ -from openapi_python_client.parser.properties import AnyProperty -from openapi_python_client.utils import PythonIdentifier - - -def test_default() -> None: - AnyProperty.build( - name="test", - required=True, - default=42, - python_name=PythonIdentifier("test", ""), - description="test", - example="test", - ) diff --git a/tests/test_parser/test_properties/test_boolean.py b/tests/test_parser/test_properties/test_boolean.py deleted file mode 100644 index 0862f1507..000000000 --- a/tests/test_parser/test_properties/test_boolean.py +++ /dev/null @@ -1,55 +0,0 @@ -import pytest - -from openapi_python_client.parser.errors import PropertyError -from openapi_python_client.parser.properties import BooleanProperty -from openapi_python_client.utils import PythonIdentifier - - -def test_invalid_default_value() -> None: - err = BooleanProperty.build( - default="not a boolean", - description=None, - example=None, - required=False, - python_name=PythonIdentifier("not_a_boolean", ""), - name="not_a_boolean", - ) - - assert isinstance(err, PropertyError) - - -@pytest.mark.parametrize( - ("value", "expected"), - ( - ("true", "True"), - ("True", "True"), - ("false", "False"), - ("False", "False"), - ), -) -def test_string_default(value, expected) -> None: - prop = BooleanProperty.build( - default=value, - description=None, - example=None, - required=False, - python_name="not_a_boolean", - name="not_a_boolean", - ) - - assert isinstance(prop, BooleanProperty) - assert prop.default.python_code == expected - - -def test_bool_default() -> None: - prop = BooleanProperty.build( - default=True, - description=None, - example=None, - required=False, - python_name="not_a_boolean", - name="not_a_boolean", - ) - - assert isinstance(prop, BooleanProperty) - assert prop.default.python_code == "True" diff --git a/tests/test_parser/test_properties/test_const.py b/tests/test_parser/test_properties/test_const.py deleted file mode 100644 index ab9e29332..000000000 --- a/tests/test_parser/test_properties/test_const.py +++ /dev/null @@ -1,28 +0,0 @@ -from openapi_python_client.parser.errors import PropertyError -from openapi_python_client.parser.properties import ConstProperty - - -def test_default_doesnt_match_const() -> None: - err = ConstProperty.build( - name="test", - required=True, - default="not the value", - python_name="test", - description=None, - const="the value", - ) - - assert isinstance(err, PropertyError) - - -def test_non_string_const() -> None: - prop = ConstProperty.build( - name="test", - required=True, - default=123, - python_name="test", - description=None, - const=123, - ) - - assert isinstance(prop, ConstProperty) diff --git a/tests/test_parser/test_properties/test_date.py b/tests/test_parser/test_properties/test_date.py deleted file mode 100644 index bcc3292b6..000000000 --- a/tests/test_parser/test_properties/test_date.py +++ /dev/null @@ -1,28 +0,0 @@ -from openapi_python_client.parser.errors import PropertyError -from openapi_python_client.parser.properties import DateProperty - - -def test_invalid_default_value(): - err = DateProperty.build( - default="not a date", - description=None, - example=None, - required=False, - python_name="not_a_date", - name="not_a_date", - ) - - assert isinstance(err, PropertyError) - - -def test_default_with_bad_type(): - err = DateProperty.build( - default=123, - description=None, - example=None, - required=False, - python_name="not_a_date", - name="not_a_date", - ) - - assert isinstance(err, PropertyError) diff --git a/tests/test_parser/test_properties/test_datetime.py b/tests/test_parser/test_properties/test_datetime.py deleted file mode 100644 index 94ea6f09c..000000000 --- a/tests/test_parser/test_properties/test_datetime.py +++ /dev/null @@ -1,28 +0,0 @@ -from openapi_python_client.parser.errors import PropertyError -from openapi_python_client.parser.properties import DateTimeProperty - - -def test_invalid_default_value(): - err = DateTimeProperty.build( - default="not a date", - description=None, - example=None, - required=False, - python_name="not_a_date", - name="not_a_date", - ) - - assert isinstance(err, PropertyError) - - -def test_default_with_bad_type(): - err = DateTimeProperty.build( - default=123, - description=None, - example=None, - required=False, - python_name="not_a_date", - name="not_a_date", - ) - - assert isinstance(err, PropertyError) diff --git a/tests/test_parser/test_properties/test_enum_property.py b/tests/test_parser/test_properties/test_enum_property.py index 282298aaf..afa9dc843 100644 --- a/tests/test_parser/test_properties/test_enum_property.py +++ b/tests/test_parser/test_properties/test_enum_property.py @@ -1,81 +1,27 @@ -from typing import Union +from openapi_python_client.parser.properties import EnumProperty +from openapi_python_client.parser.properties.schemas import Class +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import ClassName, PythonIdentifier -import pytest -import openapi_python_client.schema as oai -from openapi_python_client import Config -from openapi_python_client.parser.errors import PropertyError -from openapi_python_client.parser.properties import LiteralEnumProperty, Schemas -from openapi_python_client.parser.properties.enum_property import EnumProperty +def _class_info() -> Class: + return Class(name=ClassName(UntrustedString("MyEnum"), ""), module_name=PythonIdentifier("my_enum", "")) -PropertyClass = Union[type[EnumProperty], type[LiteralEnumProperty]] +def test_values_from_list_escapes_or_strips_control_characters(): + """Values are embedded in double-quoted string literals; newlines and control characters must be escaped""" + values = EnumProperty.values_from_list(["line\nbreak", "tab\there", "nul\x00byte"], _class_info(), []) -@pytest.fixture(params=[EnumProperty, LiteralEnumProperty]) -def property_class(request) -> PropertyClass: - return request.param + assert values["LINEBREAK"] == "linebreak" + assert values["TABHERE"] == "tab\\there" + assert values["NULBYTE"] == "nulbyte" + # Every value must be safe inside a "..." literal in generated source + for value in values.values(): + assert all(ord(c) >= 0x20 for c in value), value -def test_conflict(config: Config, property_class: PropertyClass) -> None: - schemas = Schemas() +def test_values_from_list_sanitizes_keys_with_backslash(): + """A backslash in a value must not survive into the generated identifier""" + values = EnumProperty.values_from_list(["one\\two"], _class_info(), []) - _, schemas = property_class.build( - data=oai.Schema(enum=["a"]), name="Existing", required=True, schemas=schemas, parent_name="", config=config - ) - err, new_schemas = property_class.build( - data=oai.Schema(enum=["a", "b"]), - name="Existing", - required=True, - schemas=schemas, - parent_name="", - config=config, - ) - - assert schemas == new_schemas - assert err.detail == "Found conflicting enums named Existing with incompatible values." - - -def test_bad_default_value(config: Config, property_class: PropertyClass) -> None: - data = oai.Schema(default="B", enum=["A"]) - schemas = Schemas() - - err, new_schemas = property_class.build( - data=data, name="Existing", required=True, schemas=schemas, parent_name="parent", config=config - ) - - assert schemas == new_schemas - assert err == PropertyError(detail="Value B is not valid for enum Existing", data=data) - - -def test_bad_default_type(config: Config, property_class: PropertyClass) -> None: - data = oai.Schema(default=123, enum=["A"]) - schemas = Schemas() - - err, new_schemas = property_class.build( - data=data, name="Existing", required=True, schemas=schemas, parent_name="parent", config=config - ) - - assert schemas == new_schemas - assert isinstance(err, PropertyError) - - -def test_mixed_types(config: Config, property_class: PropertyClass) -> None: - data = oai.Schema(enum=["A", 1]) - schemas = Schemas() - - err, _ = property_class.build( - data=data, name="Enum", required=True, schemas=schemas, parent_name="parent", config=config - ) - - assert isinstance(err, PropertyError) - - -def test_unsupported_type(config: Config, property_class: PropertyClass) -> None: - data = oai.Schema(enum=[1.4, 1.5]) - schemas = Schemas() - - err, _ = property_class.build( - data=data, name="Enum", required=True, schemas=schemas, parent_name="parent", config=config - ) - - assert isinstance(err, PropertyError) + assert list(values) == ["ONE_TWO"] diff --git a/tests/test_parser/test_properties/test_file.py b/tests/test_parser/test_properties/test_file.py index 87298ba03..f399e8278 100644 --- a/tests/test_parser/test_properties/test_file.py +++ b/tests/test_parser/test_properties/test_file.py @@ -3,6 +3,8 @@ def test_no_default_allowed(): + # currently this is testing an unused code path: + # https://github.com/openapi-generators/openapi-python-client/issues/1162 err = FileProperty.build( default="not none", description=None, diff --git a/tests/test_parser/test_properties/test_float.py b/tests/test_parser/test_properties/test_float.py deleted file mode 100644 index 356a61424..000000000 --- a/tests/test_parser/test_properties/test_float.py +++ /dev/null @@ -1,38 +0,0 @@ -from openapi_python_client.parser.errors import PropertyError -from openapi_python_client.parser.properties import FloatProperty -from openapi_python_client.parser.properties.protocol import Value - - -def test_invalid_default(): - err = FloatProperty.build( - default="not a float", - description=None, - example=None, - required=False, - python_name="not_a_float", - name="not_a_float", - ) - - assert isinstance(err, PropertyError) - - -def test_convert_from_string(): - assert FloatProperty.convert_value("1.0") == Value(python_code="1.0", raw_value="1.0") - assert FloatProperty.convert_value("1") == Value(python_code="1.0", raw_value="1") - - -def test_convert_from_float(): - assert FloatProperty.convert_value(1.0) == Value(python_code="1.0", raw_value=1.0) - - -def test_invalid_type_default(): - err = FloatProperty.build( - default=True, - description=None, - example=None, - required=False, - python_name="not_a_float", - name="not_a_float", - ) - - assert isinstance(err, PropertyError) diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 918defcdb..09446414f 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -1,211 +1,33 @@ -from unittest.mock import MagicMock, call - -import attr import pytest import openapi_python_client.schema as oai -from openapi_python_client.parser.errors import ParameterError, PropertyError +from openapi_python_client.parser.errors import PropertyError from openapi_python_client.parser.properties import ( - ListProperty, + Class, ReferencePath, Schemas, - StringProperty, - UnionProperty, + _propogate_removal, + build_schemas, + property_from_data, ) -from openapi_python_client.parser.properties.protocol import ModelProperty, Value -from openapi_python_client.parser.properties.schemas import Class -from openapi_python_client.schema import DataType -from openapi_python_client.utils import ClassName, PythonIdentifier +from openapi_python_client.schema.ref import Ref +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import ClassName, PythonCode, PythonIdentifier MODULE_NAME = "openapi_python_client.parser.properties" -class TestStringProperty: - def test_is_base_type(self, string_property_factory): - assert string_property_factory().is_base_type is True - - @pytest.mark.parametrize( - "required, expected", - ( - (True, "str"), - (False, "Union[Unset, str]"), - ), - ) - def test_get_type_string(self, string_property_factory, required, expected): - p = string_property_factory(required=required) - - assert p.get_type_string() == expected - - -class TestDateTimeProperty: - def test_is_base_type(self, date_time_property_factory): - assert date_time_property_factory().is_base_type is True - - @pytest.mark.parametrize("required", (True, False)) - def test_get_imports(self, date_time_property_factory, required): - p = date_time_property_factory(required=required) - - expected = { - "import datetime", - "from typing import cast", - "from dateutil.parser import isoparse", - } - if not required: - expected |= { - "from typing import Union", - "from ...types import UNSET, Unset", - } - - assert p.get_imports(prefix="...") == expected - - -class TestDateProperty: - def test_is_base_type(self, date_property_factory): - assert date_property_factory().is_base_type is True - - @pytest.mark.parametrize("required", (True, False)) - def test_get_imports(self, date_property_factory, required): - p = date_property_factory(required=required) - - expected = { - "import datetime", - "from typing import cast", - "from dateutil.parser import isoparse", - } - if not required: - expected |= { - "from typing import Union", - "from ...types import UNSET, Unset", - } - - assert p.get_imports(prefix="...") == expected - - class TestFileProperty: - def test_is_base_type(self, file_property_factory): - assert file_property_factory().is_base_type is True - @pytest.mark.parametrize("required", (True, False)) def test_get_imports(self, file_property_factory, required): p = file_property_factory(required=required) expected = { "from io import BytesIO", - "from ...types import File, FileJsonType", - } - if not required: - expected |= { - "from typing import Union", - "from ...types import UNSET, Unset", - } - - assert p.get_imports(prefix="...") == expected - - -class TestNoneProperty: - def test_is_base_type(self, none_property_factory): - assert none_property_factory().is_base_type is True - - -class TestBooleanProperty: - def test_is_base_type(self, boolean_property_factory): - assert boolean_property_factory().is_base_type is True - - -class TestAnyProperty: - def test_is_base_type(self, any_property_factory): - assert any_property_factory().is_base_type is True - - -class TestIntProperty: - def test_is_base_type(self, int_property_factory): - assert int_property_factory().is_base_type is True - - -class TestListProperty: - def test_is_base_type(self, list_property_factory): - assert list_property_factory().is_base_type is False - - @pytest.mark.parametrize("quoted", (True, False)) - def test_get_base_json_type_string_base_inner(self, list_property_factory, quoted): - p = list_property_factory() - assert p.get_base_json_type_string(quoted=quoted) == "list[str]" - - @pytest.mark.parametrize("quoted", (True, False)) - def test_get_base_json_type_string_model_inner(self, list_property_factory, model_property_factory, quoted): - m = model_property_factory() - p = list_property_factory(inner_property=m) - assert p.get_base_json_type_string(quoted=quoted) == "list[dict[str, Any]]" - - def test_get_lazy_import_base_inner(self, list_property_factory): - p = list_property_factory() - assert p.get_lazy_imports(prefix="..") == set() - - def test_get_lazy_import_model_inner(self, list_property_factory, model_property_factory): - m = model_property_factory() - p = list_property_factory(inner_property=m) - assert p.get_lazy_imports(prefix="..") == {"from ..models.my_module import MyClass"} - - @pytest.mark.parametrize( - "required, expected", - ( - (True, "list[str]"), - (False, "Union[Unset, list[str]]"), - ), - ) - def test_get_type_string_base_inner(self, list_property_factory, required, expected): - p = list_property_factory(required=required) - - assert p.get_type_string() == expected - - @pytest.mark.parametrize( - "required, expected", - ( - (True, "list['MyClass']"), - (False, "Union[Unset, list['MyClass']]"), - ), - ) - def test_get_type_string_model_inner(self, list_property_factory, model_property_factory, required, expected): - m = model_property_factory() - p = list_property_factory(required=required, inner_property=m) - - assert p.get_type_string() == expected - - @pytest.mark.parametrize( - "quoted,expected", - [ - (False, "list[str]"), - (True, "list[str]"), - ], - ) - def test_get_base_type_string_base_inner(self, list_property_factory, quoted, expected): - p = list_property_factory() - assert p.get_base_type_string(quoted=quoted) == expected - - @pytest.mark.parametrize( - "quoted,expected", - [ - (False, "list['MyClass']"), - (True, "list['MyClass']"), - ], - ) - def test_get_base_type_string_model_inner(self, list_property_factory, model_property_factory, quoted, expected): - m = model_property_factory() - p = list_property_factory(inner_property=m) - assert p.get_base_type_string(quoted=quoted) == expected - - @pytest.mark.parametrize("required", (True, False)) - def test_get_type_imports(self, list_property_factory, date_time_property_factory, required): - inner_property = date_time_property_factory() - p = list_property_factory(inner_property=inner_property, required=required) - expected = { - "import datetime", - "from typing import cast", - "from dateutil.parser import isoparse", + "from ...types import File, FileTypes", } if not required: expected |= { - "from typing import Union", "from ...types import UNSET, Unset", } @@ -213,9 +35,6 @@ def test_get_type_imports(self, list_property_factory, date_time_property_factor class TestUnionProperty: - def test_is_base_type(self, union_property_factory): - assert union_property_factory().is_base_type is False - def test_get_lazy_import_base_inner(self, union_property_factory): p = union_property_factory() assert p.get_lazy_imports(prefix="..") == set() @@ -228,11 +47,11 @@ def test_get_lazy_import_model_inner(self, union_property_factory, model_propert @pytest.mark.parametrize( "required,no_optional,json,expected", [ - (False, False, False, "Union[Unset, datetime.datetime, str]"), - (False, True, False, "Union[datetime.datetime, str]"), - (True, False, False, "Union[datetime.datetime, str]"), - (True, True, False, "Union[datetime.datetime, str]"), - (False, False, True, "Union[Unset, str]"), + (False, False, False, "datetime.datetime | str | Unset"), + (False, True, False, "datetime.datetime | str"), + (True, False, False, "datetime.datetime | str"), + (True, True, False, "datetime.datetime | str"), + (False, False, True, "str | Unset"), (False, True, True, "str"), (True, False, True, "str"), (True, True, True, "str"), @@ -253,44 +72,44 @@ def test_get_type_string( inner_properties=[date_time_property_factory(), string_property_factory()], ) - assert p.get_base_type_string() == "Union[datetime.datetime, str]" + assert p.get_base_type_string() == PythonCode("datetime.datetime | str") - assert p.get_type_string(no_optional=no_optional, json=json) == expected + assert p.get_type_string(no_optional=no_optional, json=json) == PythonCode(expected) def test_get_base_type_string_base_inners( self, union_property_factory, date_time_property_factory, string_property_factory ): p = union_property_factory(inner_properties=[date_time_property_factory(), string_property_factory()]) - assert p.get_base_type_string() == "Union[datetime.datetime, str]" + assert p.get_base_type_string() == PythonCode("datetime.datetime | str") def test_get_base_type_string_one_base_inner(self, union_property_factory, date_time_property_factory): p = union_property_factory( inner_properties=[date_time_property_factory()], ) - assert p.get_base_type_string() == "datetime.datetime" + assert p.get_base_type_string() == PythonCode("datetime.datetime") def test_get_base_type_string_one_model_inner(self, union_property_factory, model_property_factory): p = union_property_factory( inner_properties=[model_property_factory()], ) - assert p.get_base_type_string() == "'MyClass'" + assert p.get_base_type_string() == PythonCode("MyClass") def test_get_base_type_string_model_inners( self, union_property_factory, date_time_property_factory, model_property_factory ): p = union_property_factory(inner_properties=[date_time_property_factory(), model_property_factory()]) - assert p.get_base_type_string() == "Union['MyClass', datetime.datetime]" + assert p.get_base_type_string() == PythonCode("datetime.datetime | MyClass") def test_get_base_json_type_string(self, union_property_factory, date_time_property_factory): p = union_property_factory( inner_properties=[date_time_property_factory()], ) - assert p.get_base_json_type_string() == "str" + assert p.get_base_json_type_string() == PythonCode("str") @pytest.mark.parametrize("required", (True, False)) def test_get_type_imports(self, union_property_factory, date_time_property_factory, required): @@ -301,325 +120,30 @@ def test_get_type_imports(self, union_property_factory, date_time_property_facto expected = { "import datetime", "from typing import cast", - "from dateutil.parser import isoparse", - "from typing import cast, Union", } if not required: expected |= { - "from typing import Union", "from ...types import UNSET, Unset", } assert p.get_imports(prefix="...") == expected -class TestEnumProperty: - def test_is_base_type(self, enum_property_factory): - assert enum_property_factory().is_base_type is True - - @pytest.mark.parametrize( - "required, expected", - ( - (False, "Union[Unset, {}]"), - (True, "{}"), - ), - ) - def test_get_type_string(self, mocker, enum_property_factory, required, expected): - fake_class = mocker.MagicMock() - fake_class.name = "MyTestEnum" - - p = enum_property_factory(class_info=fake_class, required=required) - - assert p.get_type_string() == expected.format(fake_class.name) - assert p.get_type_string(no_optional=True) == fake_class.name - assert p.get_type_string(json=True) == expected.format("str") - - def test_get_imports(self, mocker, enum_property_factory): - fake_class = mocker.MagicMock(module_name="my_test_enum") - fake_class.name = "MyTestEnum" - prefix = "..." - - enum_property = enum_property_factory(class_info=fake_class, required=False) - - assert enum_property.get_imports(prefix=prefix) == { - f"from {prefix}models.{fake_class.module_name} import {fake_class.name}", - "from typing import Union", # Makes sure unset is handled via base class - "from ...types import UNSET, Unset", - } - - def test_values_from_list(self): - from openapi_python_client.parser.properties import EnumProperty - - data = ["abc", "123", "a23", "1bc", 4, -3, "a Thing WIth spaces", ""] - - result = EnumProperty.values_from_list(data, Class("ClassName", "module_name")) - - assert result == { - "ABC": "abc", - "VALUE_1": "123", - "A23": "a23", - "VALUE_3": "1bc", - "VALUE_4": 4, - "VALUE_NEGATIVE_3": -3, - "A_THING_WITH_SPACES": "a Thing WIth spaces", - "VALUE_7": "", - } - - def test_values_from_list_duplicate(self): - from openapi_python_client.parser.properties import EnumProperty - - data = ["abc", "123", "a23", "abc"] - - with pytest.raises(ValueError): - EnumProperty.values_from_list(data, Class("ClassName", "module_name")) - - -class TestLiteralEnumProperty: - def test_is_base_type(self, literal_enum_property_factory): - assert literal_enum_property_factory().is_base_type is True - - @pytest.mark.parametrize( - "required, expected", - ( - (False, "Union[Unset, {}]"), - (True, "{}"), - ), - ) - def test_get_type_string(self, mocker, literal_enum_property_factory, required, expected): - fake_class = mocker.MagicMock() - fake_class.name = "MyTestEnum" - - p = literal_enum_property_factory(class_info=fake_class, required=required) - - assert p.get_type_string() == expected.format(fake_class.name) - assert p.get_type_string(no_optional=True) == fake_class.name - assert p.get_type_string(json=True) == expected.format("str") - - def test_get_imports(self, mocker, literal_enum_property_factory): - fake_class = mocker.MagicMock(module_name="my_test_enum") - fake_class.name = "MyTestEnum" - prefix = "..." - - literal_enum_property = literal_enum_property_factory(class_info=fake_class, required=False) - - assert literal_enum_property.get_imports(prefix=prefix) == { - "from typing import cast", - f"from {prefix}models.{fake_class.module_name} import {fake_class.name}", - f"from {prefix}models.{fake_class.module_name} import check_my_test_enum", - "from typing import Union", # Makes sure unset is handled via base class - "from ...types import UNSET, Unset", - } - - class TestPropertyFromData: - def test_property_from_data_str_enum(self, enum_property_factory, config): - from openapi_python_client.parser.properties import Class, Schemas, property_from_data - from openapi_python_client.schema import Schema - - existing = enum_property_factory() - data = Schema(title="AnEnum", enum=["A", "B", "C"], default="B") - name = "my_enum" - required = True - - schemas = Schemas(classes_by_name={ClassName("AnEnum", prefix=""): existing}) - - prop, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config - ) - - assert prop == enum_property_factory( - name=name, - required=required, - values={"A": "A", "B": "B", "C": "C"}, - class_info=Class(name=ClassName("ParentAnEnum", ""), module_name=PythonIdentifier("parent_an_enum", "")), - value_type=str, - default=Value(python_code="ParentAnEnum.B", raw_value="B"), - ) - assert schemas != new_schemas, "Provided Schemas was mutated" - assert new_schemas.classes_by_name == { - "AnEnum": existing, - "ParentAnEnum": prop, - } - - def test_property_from_data_str_enum_with_null( - self, enum_property_factory, union_property_factory, none_property_factory, config - ): - from openapi_python_client.parser.properties import Class, Schemas, property_from_data - from openapi_python_client.schema import Schema - - existing = enum_property_factory() - data = Schema(title="AnEnum", enum=["A", "B", "C", None], default="B") - name = "my_enum" - required = True - - schemas = Schemas(classes_by_name={ClassName("AnEnum", ""): existing}) - - prop, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config - ) - - # None / null is removed from enum, and property is now nullable - assert isinstance(prop, UnionProperty), "Enums with None should be converted to UnionProperties" - enum_prop = enum_property_factory( - name="my_enum_type_1", - required=required, - values={"A": "A", "B": "B", "C": "C"}, - class_info=Class(name=ClassName("ParentAnEnum", ""), module_name=PythonIdentifier("parent_an_enum", "")), - value_type=str, - default=Value(python_code="ParentAnEnum.B", raw_value="B"), - ) - none_property = none_property_factory(name="my_enum_type_0", required=required) - assert prop == union_property_factory( - name=name, - default=Value(python_code="ParentAnEnum.B", raw_value="B"), - inner_properties=[none_property, enum_prop], - ) - assert schemas != new_schemas, "Provided Schemas was mutated" - assert new_schemas.classes_by_name == { - "AnEnum": existing, - "ParentAnEnum": enum_prop, - } - - def test_property_from_data_null_enum(self, enum_property_factory, none_property_factory, config): - from openapi_python_client.parser.properties import Schemas, property_from_data - from openapi_python_client.schema import Schema - - data = Schema(title="AnEnumWithOnlyNull", enum=[None], default=None) - name = "my_enum" - required = True - - schemas = Schemas() - - prop, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config - ) - - assert prop == none_property_factory( - name="my_enum", required=required, default=Value(python_code="None", raw_value="None") - ) - - def test_property_from_data_int_enum(self, enum_property_factory, config): - from openapi_python_client.parser.properties import Class, Schemas, property_from_data - from openapi_python_client.schema import Schema - - name = "my_enum" - required = True - data = Schema.model_construct(title="anEnum", enum=[1, 2, 3], default=3) - - existing = enum_property_factory() - schemas = Schemas(classes_by_name={ClassName("AnEnum", ""): existing}) - - prop, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config - ) - - assert prop == enum_property_factory( - name=name, - required=required, - values={"VALUE_1": 1, "VALUE_2": 2, "VALUE_3": 3}, - class_info=Class(name=ClassName("ParentAnEnum", ""), module_name=PythonIdentifier("parent_an_enum", "")), - value_type=int, - default=Value(python_code="ParentAnEnum.VALUE_3", raw_value=3), - ) - assert schemas != new_schemas, "Provided Schemas was mutated" - assert new_schemas.classes_by_name == { - "AnEnum": existing, - "ParentAnEnum": prop, - } - - def test_property_from_data_ref_enum(self, enum_property_factory, config): - from openapi_python_client.parser.properties import Class, Schemas, property_from_data - - name = "some_enum" - data = oai.Reference.model_construct(ref="#/components/schemas/MyEnum") - existing_enum = enum_property_factory( - name="an_enum", - required=False, - values={"A": "a"}, - class_info=Class(name="MyEnum", module_name="my_enum"), - ) - schemas = Schemas(classes_by_reference={"/components/schemas/MyEnum": existing_enum}) - - prop, new_schemas = property_from_data( - name=name, required=False, data=data, schemas=schemas, parent_name="", config=config - ) - - assert prop == enum_property_factory( - name="some_enum", - required=False, - values={"A": "a"}, - class_info=Class(name="MyEnum", module_name="my_enum"), - ) - assert schemas == new_schemas - - def test_property_from_data_ref_enum_with_overridden_default(self, enum_property_factory, config): - from openapi_python_client.parser.properties import Class, Schemas, property_from_data - - name = "some_enum" - required = False - data = oai.Schema.model_construct( - default="b", allOf=[oai.Reference.model_construct(ref="#/components/schemas/MyEnum")] - ) - existing_enum = enum_property_factory( - name="an_enum", - default=Value(python_code="MyEnum.A", raw_value="A"), - required=required, - values={"A": "a", "B": "b"}, - class_info=Class(name=ClassName("MyEnum", ""), module_name=PythonIdentifier("my_enum", "")), - ) - schemas = Schemas(classes_by_reference={ReferencePath("/components/schemas/MyEnum"): existing_enum}) - - prop, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="", config=config - ) - new_schemas = attr.evolve(new_schemas, models_to_process=[]) # intermediate state irrelevant to this test - - assert prop == enum_property_factory( - name="some_enum", - default=Value(python_code="MyEnum.B", raw_value="b"), - required=required, - values={"A": "a", "B": "b"}, - class_info=Class(name=ClassName("MyEnum", ""), module_name=PythonIdentifier("my_enum", "")), - ) - assert schemas == new_schemas - - def test_property_from_data_ref_enum_with_invalid_default(self, enum_property_factory, config): - from openapi_python_client.parser.properties import Class, Schemas, property_from_data - - name = "some_enum" - data = oai.Schema.model_construct( - default="x", allOf=[oai.Reference.model_construct(ref="#/components/schemas/MyEnum")] - ) - existing_enum = enum_property_factory( - name="an_enum", - default=Value(python_code="MyEnum.A", raw_value="A"), - values={"A": "a", "B": "b"}, - class_info=Class(name=ClassName("MyEnum", ""), module_name=PythonIdentifier("my_enum", "")), - python_name=PythonIdentifier("an_enum", ""), - ) - schemas = Schemas(classes_by_reference={ReferencePath("/components/schemas/MyEnum"): existing_enum}) - - prop, new_schemas = property_from_data( - name=name, required=False, data=data, schemas=schemas, parent_name="", config=config - ) - - assert schemas == new_schemas - assert prop == PropertyError(data=data, detail="Value x is not valid for enum an_enum") - def test_property_from_data_ref_model(self, model_property_factory, config): - from openapi_python_client.parser.properties import Class, Schemas, property_from_data - name = "new_name" required = False - class_name = ClassName("MyModel", "") - data = oai.Reference.model_construct(ref=f"#/components/schemas/{class_name}") + class_name = ClassName(UntrustedString("MyModel"), "") + data = oai.Reference.model_construct(ref=Ref(f"#/components/schemas/{class_name}")) class_info = Class(name=class_name, module_name=PythonIdentifier("my_model", "")) existing_model = model_property_factory( name="old_name", class_info=class_info, ) - schemas = Schemas(classes_by_reference={ReferencePath(f"/components/schemas/{class_name}"): existing_model}) + schemas = Schemas( + classes_by_reference={ReferencePath(UntrustedString(f"/components/schemas/{class_name}")): existing_model} + ) prop, new_schemas = property_from_data( name=name, required=required, data=data, schemas=schemas, parent_name="", config=config @@ -633,8 +157,6 @@ def test_property_from_data_ref_model(self, model_property_factory, config): assert schemas == new_schemas def test_property_from_data_ref_not_found(self, mocker): - from openapi_python_client.parser.properties import PropertyError, Schemas, property_from_data - data = oai.Reference.model_construct(ref="a/b/c") parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") schemas = Schemas() @@ -650,12 +172,10 @@ def test_property_from_data_ref_not_found(self, mocker): @pytest.mark.parametrize("references_exist", (True, False)) def test_property_from_data_ref(self, any_property_factory, references_exist, config): - from openapi_python_client.parser.properties import Schemas, property_from_data - name = "new_name" required = False - ref_path = "/components/schemas/RefName" - data = oai.Reference.model_construct(ref=f"#{ref_path}") + ref_path = ReferencePath(UntrustedString("/components/schemas/RefName")) + data = oai.Reference.model_construct(ref=Ref(f"#{ref_path.get_untrusted_value()}")) roots = {"new_root"} existing_property = any_property_factory(name="old_name") @@ -670,202 +190,9 @@ def test_property_from_data_ref(self, any_property_factory, references_exist, co assert schemas == new_schemas assert schemas.dependencies == {ref_path: {*roots, *references.get(ref_path, set())}} - def test_property_from_data_invalid_ref(self, mocker): - from openapi_python_client.parser.properties import PropertyError, Schemas, property_from_data - - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Reference.model_construct(ref=mocker.MagicMock()) - parse_reference_path = mocker.patch( - f"{MODULE_NAME}.parse_reference_path", return_value=PropertyError(detail="bad stuff") - ) - schemas = Schemas() - - prop, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=mocker.MagicMock() - ) - - parse_reference_path.assert_called_once_with(data.ref) - assert prop == PropertyError(data=data, detail="bad stuff") - assert schemas == new_schemas - - def test_property_from_data_array(self, config): - from openapi_python_client.parser.properties import Schemas, property_from_data - - name = "a_list_prop" - required = True - data = oai.Schema( - type=DataType.ARRAY, - items=oai.Schema(type=DataType.STRING), - ) - schemas = Schemas() - - response = property_from_data( - name=name, - required=required, - data=data, - schemas=schemas, - parent_name="parent", - config=config, - )[0] - - assert isinstance(response, ListProperty) - assert isinstance(response.inner_property, StringProperty) - - def test_property_from_data_union(self, config): - from openapi_python_client.parser.properties import Schemas, property_from_data - - name = "union_prop" - required = True - data = oai.Schema( - anyOf=[oai.Schema(type=DataType.NUMBER)], - oneOf=[ - oai.Schema(type=DataType.INTEGER), - ], - ) - schemas = Schemas() - - response = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config - )[0] - - assert isinstance(response, UnionProperty) - assert len(response.inner_properties) == 2 - - def test_property_from_data_list_of_types(self, config): - from openapi_python_client.parser.properties import Schemas, property_from_data - - name = "union_prop" - required = True - data = oai.Schema( - type=[DataType.NUMBER, DataType.NULL], - ) - schemas = Schemas() - - response = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config - )[0] - - assert isinstance(response, UnionProperty) - assert len(response.inner_properties) == 2 - - def test_property_from_data_union_of_one_element(self, model_property_factory, config): - from openapi_python_client.parser.properties import Schemas, property_from_data - - name = "new_name" - required = False - class_name = "MyModel" - existing_model: ModelProperty = model_property_factory() - schemas = Schemas(classes_by_reference={f"/{class_name}": existing_model}) - - data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref=f"#/{class_name}")], - ) - - prop, schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config - ) - - assert prop == attr.evolve(existing_model, name=name, required=required, python_name=PythonIdentifier(name, "")) - - def test_property_from_data_no_valid_props_in_data(self, any_property_factory): - from openapi_python_client.parser.properties import Schemas, property_from_data - - schemas = Schemas() - data = oai.Schema() - name = "blah" - - prop, new_schemas = property_from_data( - name=name, required=True, data=data, schemas=schemas, parent_name="parent", config=MagicMock() - ) - - assert prop == any_property_factory(name=name, required=True, default=None) - assert new_schemas == schemas - class TestStringBasedProperty: - @pytest.mark.parametrize("required", (True, False)) - def test_no_format(self, string_property_factory, required, config): - from openapi_python_client.parser.properties import property_from_data - - name = "some_prop" - data = oai.Schema.model_construct(type="string", default="hello world") - - p, _ = property_from_data( - name=name, required=required, data=data, parent_name=None, config=config, schemas=Schemas() - ) - - assert p == string_property_factory( - name=name, required=required, default=StringProperty.convert_value("hello world") - ) - - def test_datetime_format(self, date_time_property_factory, config): - from openapi_python_client.parser.properties import property_from_data - - name = "datetime_prop" - required = True - data = oai.Schema.model_construct(type="string", schema_format="date-time", default="2020-11-06T12:00:00") - - p, _ = property_from_data( - name=name, required=required, data=data, schemas=Schemas(), config=config, parent_name="" - ) - - assert p == date_time_property_factory( - name=name, - required=required, - default=Value(python_code=f"isoparse('{data.default}')", raw_value=data.default), - ) - - def test_datetime_bad_default(self, config): - from openapi_python_client.parser.properties import property_from_data - - name = "datetime_prop" - required = True - data = oai.Schema.model_construct(type="string", schema_format="date-time", default="a") - - result, _ = property_from_data( - name=name, required=required, data=data, schemas=Schemas(), config=config, parent_name="" - ) - - assert isinstance(result, PropertyError) - assert result.detail.startswith("Invalid datetime") - - def test_date_format(self, date_property_factory, config): - from openapi_python_client.parser.properties import property_from_data - - name = "date_prop" - required = True - - data = oai.Schema.model_construct(type="string", schema_format="date", default="2020-11-06") - - p, _ = property_from_data( - name=name, required=required, data=data, schemas=Schemas(), config=config, parent_name="" - ) - - assert p == date_property_factory( - name=name, - required=required, - default=Value(python_code=f"isoparse('{data.default}').date()", raw_value=data.default), - ) - - def test_date_format_bad_default(self, config): - from openapi_python_client.parser.properties import property_from_data - - name = "date_prop" - required = True - - data = oai.Schema.model_construct(type="string", schema_format="date", default="a") - - p, _ = property_from_data( - name=name, required=required, data=data, schemas=Schemas(), config=config, parent_name="" - ) - - assert isinstance(p, PropertyError) - assert p.detail.startswith("Invalid date") - def test__string_based_property_binary_format(self, file_property_factory, config): - from openapi_python_client.parser.properties import property_from_data - name = "file_prop" required = True data = oai.Schema.model_construct(type="string", schema_format="binary", default="a") @@ -875,148 +202,30 @@ def test__string_based_property_binary_format(self, file_property_factory, confi ) assert p == file_property_factory(name=name, required=required) - def test__string_based_property_unsupported_format(self, string_property_factory, config): - from openapi_python_client.parser.properties import property_from_data - - name = "unknown" - required = True - data = oai.Schema.model_construct(type="string", schema_format="blah") - - p, _ = property_from_data( - name=name, required=required, data=data, schemas=Schemas(), config=config, parent_name="" - ) - - assert p == string_property_factory(name=name, required=required) - - -class TestCreateSchemas: - def test_skips_references_and_keeps_going(self, mocker, config): - from openapi_python_client.parser.properties import Schemas, _create_schemas - from openapi_python_client.schema import Reference, Schema - - components = {"a_ref": Reference.model_construct(), "a_schema": Schema.model_construct()} - update_schemas_with_data = mocker.patch(f"{MODULE_NAME}.update_schemas_with_data") - parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") - schemas = Schemas() - - result = _create_schemas(components=components, schemas=schemas, config=config) - # Should not even try to parse a path for the Reference - parse_reference_path.assert_called_once_with("#/components/schemas/a_schema") - update_schemas_with_data.assert_called_once_with( - ref_path=parse_reference_path.return_value, - config=config, - data=components["a_schema"], - schemas=Schemas( - errors=[PropertyError(detail="Reference schemas are not supported.", data=components["a_ref"])] - ), - ) - assert result == update_schemas_with_data.return_value - - def test_records_bad_uris_and_keeps_going(self, mocker, config): - from openapi_python_client.parser.properties import Schemas, _create_schemas - from openapi_python_client.schema import Schema - - components = {"first": Schema.model_construct(), "second": Schema.model_construct()} - update_schemas_with_data = mocker.patch(f"{MODULE_NAME}.update_schemas_with_data") - parse_reference_path = mocker.patch( - f"{MODULE_NAME}.parse_reference_path", side_effect=[PropertyError(detail="some details"), "a_path"] - ) - schemas = Schemas() - - result = _create_schemas(components=components, schemas=schemas, config=config) - parse_reference_path.assert_has_calls( - [ - call("#/components/schemas/first"), - call("#/components/schemas/second"), - ] - ) - update_schemas_with_data.assert_called_once_with( - ref_path="a_path", - config=config, - data=components["second"], - schemas=Schemas(errors=[PropertyError(detail="some details", data=components["first"])]), - ) - assert result == update_schemas_with_data.return_value - - def test_retries_failing_properties_while_making_progress(self, mocker, config): - from openapi_python_client.parser.properties import Schemas, _create_schemas - from openapi_python_client.schema import Schema - - components = {"first": Schema.model_construct(), "second": Schema.model_construct()} - update_schemas_with_data = mocker.patch( - f"{MODULE_NAME}.update_schemas_with_data", side_effect=[PropertyError(), Schemas(), PropertyError()] - ) - parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") - schemas = Schemas() - - result = _create_schemas(components=components, schemas=schemas, config=config) - parse_reference_path.assert_has_calls( - [ - call("#/components/schemas/first"), - call("#/components/schemas/second"), - call("#/components/schemas/first"), - ] - ) - assert update_schemas_with_data.call_count == 3 - assert result.errors == [PropertyError()] - class TestProcessModels: - def test_detect_recursive_allof_reference_no_retry(self, mocker, model_property_factory, config): - from openapi_python_client.parser.properties import Class, _process_models - from openapi_python_client.schema import Reference - - class_name = ClassName("class_name", "") - recursive_model = model_property_factory( - class_info=Class(name=class_name, module_name=PythonIdentifier("module_name", "")) - ) - second_model = model_property_factory() - schemas = Schemas( - classes_by_name={ - "recursive": recursive_model, - "second": second_model, - }, - models_to_process=[recursive_model, second_model], - ) - recursion_error = PropertyError(data=Reference.model_construct(ref=f"#/{class_name}")) - process_model = mocker.patch(f"{MODULE_NAME}.process_model", side_effect=[recursion_error, schemas]) - process_model_errors = mocker.patch(f"{MODULE_NAME}._process_model_errors", return_value=["error"]) - - result = _process_models(schemas=schemas, config=config) - - process_model.assert_has_calls( - [ - call(recursive_model, schemas=schemas, config=config), - call(schemas.classes_by_name["second"], schemas=schemas, config=config), - ] - ) - assert process_model_errors.was_called_once_with([(recursive_model, recursion_error)]) - assert all(error in result.errors for error in process_model_errors.return_value) - assert "\n\nRecursive allOf reference found" in recursion_error.detail - def test_resolve_reference_to_single_allof_reference(self, config, model_property_factory): # test for https://github.com/openapi-generators/openapi-python-client/issues/1091 - from openapi_python_client.parser.properties import Schemas, build_schemas components = { - "Model1": oai.Schema.model_construct( + UntrustedString("Model1"): oai.Schema.model_construct( type="object", properties={ - "prop1": oai.Schema.model_construct(type="string"), + UntrustedString("prop1"): oai.Schema.model_construct(type="string"), }, ), - "Model2": oai.Schema.model_construct( + UntrustedString("Model2"): oai.Schema.model_construct( allOf=[ - oai.Reference.model_construct(ref="#/components/schemas/Model1"), + oai.Reference.model_construct(ref=Ref("#/components/schemas/Model1")), ] ), - "Model3": oai.Schema.model_construct( + UntrustedString("Model3"): oai.Schema.model_construct( allOf=[ - oai.Reference.model_construct(ref="#/components/schemas/Model2"), + oai.Reference.model_construct(ref=Ref("#/components/schemas/Model2")), oai.Schema.model_construct( type="object", properties={ - "prop2": oai.Schema.model_construct(type="string"), + UntrustedString("prop2"): oai.Schema.model_construct(type="string"), }, ), ], @@ -1039,8 +248,8 @@ def test_resolve_reference_to_single_allof_reference(self, config, model_propert "/components/schemas/Model3", } assert ( - result.classes_by_reference["/components/schemas/Model2"].class_info - == result.classes_by_reference["/components/schemas/Model1"].class_info + result.classes_by_reference[ReferencePath(UntrustedString("/components/schemas/Model2"))].class_info + == result.classes_by_reference[ReferencePath(UntrustedString("/components/schemas/Model1"))].class_info ) # Verify that Model3 extended the properties from Model1 @@ -1049,12 +258,9 @@ def test_resolve_reference_to_single_allof_reference(self, config, model_propert class TestPropogateRemoval: def test_propogate_removal_class_name(self): - from openapi_python_client.parser.properties import ReferencePath, _propogate_removal - from openapi_python_client.utils import ClassName - - root = ClassName("ClassName", "") - ref_path = ReferencePath("/reference") - other_class_name = ClassName("OtherClassName", "") + root = ClassName(UntrustedString("ClassName"), "") + ref_path = ReferencePath(UntrustedString("/reference")) + other_class_name = ClassName(UntrustedString("OtherClassName"), "") schemas = Schemas( classes_by_name={root: None, other_class_name: None}, classes_by_reference={ref_path: None}, @@ -1069,12 +275,9 @@ def test_propogate_removal_class_name(self): assert not error.detail def test_propogate_removal_ref_path(self): - from openapi_python_client.parser.properties import ReferencePath, _propogate_removal - from openapi_python_client.utils import ClassName - - root = ReferencePath("/root/reference") - class_name = ClassName("ClassName", "") - ref_path = ReferencePath("/ref/path") + root = ReferencePath(UntrustedString("/root/reference")) + class_name = ClassName(UntrustedString("ClassName"), "") + ref_path = ReferencePath(UntrustedString("/ref/path")) schemas = Schemas( classes_by_name={class_name: None}, classes_by_reference={root: None, ref_path: None}, @@ -1086,31 +289,28 @@ def test_propogate_removal_ref_path(self): assert schemas.classes_by_name == {} assert schemas.classes_by_reference == {} - assert error.detail == f"\n{root}\n{ref_path}" + assert error.detail == f"\n{root.get_untrusted_value()}\n{ref_path.get_untrusted_value()}" def test_propogate_removal_ref_path_no_refs(self): - from openapi_python_client.parser.properties import ReferencePath, _propogate_removal - from openapi_python_client.utils import ClassName - - root = ReferencePath("/root/reference") - class_name = ClassName("ClassName", "") - ref_path = ReferencePath("/ref/path") - schemas = Schemas(classes_by_name={class_name: None}, classes_by_reference={root: None, ref_path: None}) + root = ReferencePath(UntrustedString("/root/reference")) + class_name = ClassName(UntrustedString("ClassName"), "") + ref_path = ReferencePath(UntrustedString("/ref/path")) + schemas = Schemas( + classes_by_name={class_name: None}, + classes_by_reference={root: None, ref_path: None}, + ) error = PropertyError() _propogate_removal(root=root, schemas=schemas, error=error) assert schemas.classes_by_name == {class_name: None} assert schemas.classes_by_reference == {ref_path: None} - assert error.detail == f"\n{root}" + assert error.detail == f"\n{root.get_untrusted_value()}" def test_propogate_removal_ref_path_already_removed(self): - from openapi_python_client.parser.properties import ReferencePath, _propogate_removal - from openapi_python_client.utils import ClassName - - root = ReferencePath("/root/reference") - class_name = ClassName("ClassName", "") - ref_path = ReferencePath("/ref/path") + root = ReferencePath(UntrustedString("/root/reference")) + class_name = ClassName(UntrustedString("ClassName"), "") + ref_path = ReferencePath(UntrustedString("/ref/path")) schemas = Schemas( classes_by_name={class_name: None}, classes_by_reference={ref_path: None}, @@ -1123,120 +323,3 @@ def test_propogate_removal_ref_path_already_removed(self): assert schemas.classes_by_name == {class_name: None} assert schemas.classes_by_reference == {ref_path: None} assert not error.detail - - -def test_process_model_errors(mocker, model_property_factory): - from openapi_python_client.parser.properties import _process_model_errors - - propogate_removal = mocker.patch(f"{MODULE_NAME}._propogate_removal") - model_errors = [ - (model_property_factory(roots={"root1", "root2"}), PropertyError(detail="existing detail")), - (model_property_factory(roots=set()), PropertyError()), - (model_property_factory(roots={"root1", "root3"}), PropertyError(detail="other existing detail")), - ] - schemas = Schemas() - - result = _process_model_errors(model_errors, schemas=schemas) - - propogate_removal.assert_has_calls( - [call(root=root, schemas=schemas, error=error) for model, error in model_errors for root in model.roots] - ) - assert result == [error for _, error in model_errors] - assert all("\n\nFailure to process schema has resulted in the removal of:" in error.detail for error in result) - - -class TestBuildParameters: - def test_skips_references_and_keeps_going(self, mocker, config): - from openapi_python_client.parser.properties import Parameters, build_parameters - from openapi_python_client.schema import Parameter, Reference - - parameters = { - "reference": Reference(ref="#/components/parameters/another_parameter"), - "defined": Parameter( - name="page", - param_in="query", - required=False, - style="form", - explode=True, - schema=oai.Schema(type="integer", default=0), - ), - } - - update_parameters_with_data = mocker.patch(f"{MODULE_NAME}.update_parameters_with_data") - parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") - - result = build_parameters(components=parameters, parameters=Parameters(), config=config) - # Should not even try to parse a path for the Reference - parse_reference_path.assert_called_once_with("#/components/parameters/defined") - update_parameters_with_data.assert_called_once_with( - ref_path=parse_reference_path.return_value, - data=parameters["defined"], - parameters=Parameters( - errors=[ParameterError(detail="Reference parameters are not supported.", data=parameters["reference"])] - ), - config=config, - ) - assert result == update_parameters_with_data.return_value - - def test_records_bad_uris_and_keeps_going(self, mocker, config): - from openapi_python_client.parser.properties import Parameters, build_parameters - from openapi_python_client.schema import Parameter - - parameters = {"first": Parameter.model_construct(), "second": Parameter.model_construct()} - update_parameters_with_data = mocker.patch(f"{MODULE_NAME}.update_parameters_with_data") - parse_reference_path = mocker.patch( - f"{MODULE_NAME}.parse_reference_path", side_effect=[ParameterError(detail="some details"), "a_path"] - ) - - result = build_parameters(components=parameters, parameters=Parameters(), config=config) - parse_reference_path.assert_has_calls( - [ - call("#/components/parameters/first"), - call("#/components/parameters/second"), - ] - ) - update_parameters_with_data.assert_called_once_with( - ref_path="a_path", - data=parameters["second"], - parameters=Parameters(errors=[ParameterError(detail="some details", data=parameters["first"])]), - config=config, - ) - assert result == update_parameters_with_data.return_value - - def test_retries_failing_parameters_while_making_progress(self, mocker, config): - from openapi_python_client.parser.properties import Parameters, build_parameters - from openapi_python_client.schema import Parameter - - parameters = {"first": Parameter.model_construct(), "second": Parameter.model_construct()} - update_parameters_with_data = mocker.patch( - f"{MODULE_NAME}.update_parameters_with_data", side_effect=[ParameterError(), Parameters(), ParameterError()] - ) - - parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") - result = build_parameters(components=parameters, parameters=Parameters(), config=config) - parse_reference_path.assert_has_calls( - [ - call("#/components/parameters/first"), - call("#/components/parameters/second"), - call("#/components/parameters/first"), - ] - ) - assert update_parameters_with_data.call_count == 3 - assert result.errors == [ParameterError()] - - -def test_build_schemas(mocker, config): - from openapi_python_client.parser.properties import Schemas, build_schemas - from openapi_python_client.schema import Reference, Schema - - create_schemas = mocker.patch(f"{MODULE_NAME}._create_schemas") - process_models = mocker.patch(f"{MODULE_NAME}._process_models") - - components = {"a_ref": Reference.model_construct(), "a_schema": Schema.model_construct()} - schemas = Schemas() - - result = build_schemas(components=components, schemas=schemas, config=config) - - create_schemas.assert_called_once_with(components=components, schemas=schemas, config=config) - process_models.assert_called_once_with(schemas=create_schemas.return_value, config=config) - assert result == process_models.return_value diff --git a/tests/test_parser/test_properties/test_int.py b/tests/test_parser/test_properties/test_int.py deleted file mode 100644 index 7f9953761..000000000 --- a/tests/test_parser/test_properties/test_int.py +++ /dev/null @@ -1,34 +0,0 @@ -from openapi_python_client.parser.errors import PropertyError -from openapi_python_client.parser.properties import IntProperty -from openapi_python_client.parser.properties.protocol import Value -from openapi_python_client.utils import PythonIdentifier - - -def test_invalid_default(): - err = IntProperty.build( - default="not a float", - description=None, - example=None, - required=False, - python_name="not_a_float", - name="not_a_float", - ) - - assert isinstance(err, PropertyError) - - -def test_convert_from_string(): - assert IntProperty.convert_value("1") == Value(python_code="1", raw_value="1") - - -def test_invalid_type_default(): - err = IntProperty.build( - default=True, - description=None, - example=None, - required=False, - python_name=PythonIdentifier("not_a_float", ""), - name="not_a_float", - ) - - assert isinstance(err, PropertyError) diff --git a/tests/test_parser/test_properties/test_list_property.py b/tests/test_parser/test_properties/test_list_property.py deleted file mode 100644 index 60fb0a35d..000000000 --- a/tests/test_parser/test_properties/test_list_property.py +++ /dev/null @@ -1,178 +0,0 @@ -import attr - -import openapi_python_client.schema as oai -from openapi_python_client.parser.errors import ParseError, PropertyError -from openapi_python_client.parser.properties import ListProperty -from openapi_python_client.parser.properties.schemas import ReferencePath -from openapi_python_client.schema import DataType -from openapi_python_client.utils import ClassName - - -def test_build_list_property_no_items(config): - from openapi_python_client.parser import properties - - name = "list_prop" - required = True - data = oai.Schema(type=DataType.ARRAY) - schemas = properties.Schemas() - - p, new_schemas = ListProperty.build( - name=name, - required=required, - data=data, - schemas=schemas, - parent_name="parent", - config=config, - process_properties=True, - roots={ReferencePath("root")}, - ) - - assert p == PropertyError(data=data, detail="type array must have items or prefixItems defined") - assert new_schemas == schemas - - -def test_build_list_property_invalid_items(config): - from openapi_python_client.parser import properties - - name = "name" - required = True - data = oai.Schema( - type=DataType.ARRAY, - items=oai.Reference.model_validate({"$ref": "doesnt exist"}), - ) - schemas = properties.Schemas(errors=[ParseError("error")]) - process_properties = False - roots: set[ReferencePath | ClassName] = {ReferencePath("root")} - - p, new_schemas = ListProperty.build( - name=name, - required=required, - data=data, - schemas=attr.evolve(schemas), - parent_name="parent", - config=config, - roots=roots, - process_properties=process_properties, - ) - - assert isinstance(p, PropertyError) - assert p.data == data.items - assert p.header.startswith(f"invalid data in items of array {name}") - assert new_schemas == schemas - - -def test_build_list_property(any_property_factory, config): - from openapi_python_client.parser import properties - - name = "prop" - data = oai.Schema( - type=DataType.ARRAY, - items=oai.Schema(), - ) - schemas = properties.Schemas(errors=[ParseError("error")]) - - p, new_schemas = ListProperty.build( - name=name, - required=True, - data=data, - schemas=schemas, - parent_name="parent", - config=config, - roots={ReferencePath("root")}, - process_properties=True, - ) - - assert isinstance(p, properties.ListProperty) - assert p.inner_property == any_property_factory(name=f"{name}_item") - assert new_schemas == schemas - - -def test_build_list_property_single_prefix_item(any_property_factory, config): - from openapi_python_client.parser import properties - - name = "prop" - data = oai.Schema( - type=DataType.ARRAY, - prefixItems=[oai.Schema()], - ) - schemas = properties.Schemas(errors=[ParseError("error")]) - - p, new_schemas = ListProperty.build( - name=name, - required=True, - data=data, - schemas=schemas, - parent_name="parent", - config=config, - roots={ReferencePath("root")}, - process_properties=True, - ) - - assert isinstance(p, properties.ListProperty) - assert p.inner_property == any_property_factory(name=f"{name}_item") - assert new_schemas == schemas - - -def test_build_list_property_items_and_prefix_items( - union_property_factory, - string_property_factory, - none_property_factory, - int_property_factory, - config, -): - from openapi_python_client.parser import properties - - name = "list_prop" - required = True - data = oai.Schema( - type=DataType.ARRAY, - items=oai.Schema(type=DataType.INTEGER), - prefixItems=[oai.Schema(type=DataType.STRING), oai.Schema(type=DataType.NULL)], - ) - schemas = properties.Schemas() - - p, new_schemas = ListProperty.build( - name=name, - required=required, - data=data, - schemas=schemas, - parent_name="parent", - config=config, - process_properties=True, - roots={ReferencePath("root")}, - ) - - assert isinstance(p, properties.ListProperty) - assert p.inner_property == union_property_factory( - name=f"{name}_item", - inner_properties=[ - string_property_factory(name=f"{name}_item_type_0"), - none_property_factory(name=f"{name}_item_type_1"), - int_property_factory(name=f"{name}_item_type_2"), - ], - ) - assert new_schemas == schemas - - -def test_build_list_property_prefix_items_only(any_property_factory, config): - from openapi_python_client.parser import properties - - name = "list_prop" - required = True - data = oai.Schema(type=DataType.ARRAY, prefixItems=[oai.Schema()]) - schemas = properties.Schemas() - - p, new_schemas = ListProperty.build( - name=name, - required=required, - data=data, - schemas=schemas, - parent_name="parent", - config=config, - process_properties=True, - roots={ReferencePath("root")}, - ) - - assert isinstance(p, properties.ListProperty) - assert p.inner_property == any_property_factory(name=f"{name}_item") - assert new_schemas == schemas diff --git a/tests/test_parser/test_properties/test_merge_properties.py b/tests/test_parser/test_properties/test_merge_properties.py index 819f9ec26..ea77e080b 100644 --- a/tests/test_parser/test_properties/test_merge_properties.py +++ b/tests/test_parser/test_properties/test_merge_properties.py @@ -10,6 +10,7 @@ from openapi_python_client.parser.properties.protocol import Value from openapi_python_client.parser.properties.schemas import Class from openapi_python_client.parser.properties.string import StringProperty +from openapi_python_client.strings import PythonCode MODULE_NAME = "openapi_python_client.parser.properties.merge_properties" @@ -23,9 +24,9 @@ def test_merge_basic_attributes_same_type( model_property_factory, ): basic_props = [ - boolean_property_factory(default=Value(python_code="True", raw_value="True")), - int_property_factory(default=Value("1", 1)), - float_property_factory(default=Value("1.5", 1.5)), + boolean_property_factory(default=Value(python_code=PythonCode("True"), raw_value="True")), + int_property_factory(default=Value(PythonCode("1"), 1)), + float_property_factory(default=Value(PythonCode("1.5"), 1.5)), string_property_factory(default=StringProperty.convert_value("x")), list_property_factory(), model_property_factory(), @@ -70,14 +71,14 @@ def test_incompatible_types( def test_merge_int_with_float(int_property_factory, float_property_factory): int_prop = int_property_factory(description="desc1") - float_prop = float_property_factory(default=Value("2", 2), description="desc2") + float_prop = float_property_factory(default=Value(PythonCode("2"), 2), description="desc2") assert merge_properties(int_prop, float_prop) == ( - evolve(int_prop, default=Value("2", 2), description=float_prop.description) + evolve(int_prop, default=Value(PythonCode("2"), 2), description=float_prop.description) ) - assert merge_properties(float_prop, int_prop) == evolve(int_prop, default=Value("2", 2)) + assert merge_properties(float_prop, int_prop) == evolve(int_prop, default=Value(PythonCode("2"), 2)) - float_prop_with_non_int_default = evolve(float_prop, default=Value("2.5", 2.5)) + float_prop_with_non_int_default = evolve(float_prop, default=Value(PythonCode("2.5"), 2.5)) error = merge_properties(int_prop, float_prop_with_non_int_default) assert isinstance(error, PropertyError), "Expected invalid default to error" assert error.detail == "Invalid int value: 2.5" @@ -93,9 +94,9 @@ def test_merge_with_any( ): original_desc = "description" props = [ - boolean_property_factory(default=Value("True", "True"), description=original_desc), - int_property_factory(default=Value("1", "1"), description=original_desc), - float_property_factory(default=Value("1.5", "1.5"), description=original_desc), + boolean_property_factory(default=Value(PythonCode("True"), "True"), description=original_desc), + int_property_factory(default=Value(PythonCode("1"), "1"), description=original_desc), + float_property_factory(default=Value(PythonCode("1.5"), "1.5"), description=original_desc), string_property_factory(default=StringProperty.convert_value("x"), description=original_desc), model_property_factory(description=original_desc), ] @@ -151,10 +152,10 @@ def test_merge_enums(literal_enums, enum_property_factory, literal_enum_property def test_merge_string_with_string_enum( literal_enums, string_property_factory, enum_property_factory, literal_enum_property_factory ): - string_prop = string_property_factory(default=Value("A", "A"), description="desc1", example="example1") + string_prop = string_property_factory(default=Value(PythonCode("A"), "A"), description="desc1", example="example1") enum_prop = ( literal_enum_property_factory( - default=Value("'B'", "B"), + default=Value(PythonCode("'B'"), "B"), description="desc2", example="example2", values={"A", "B"}, @@ -162,7 +163,7 @@ def test_merge_string_with_string_enum( ) if literal_enums else enum_property_factory( - default=Value("test.B", "B"), + default=Value(PythonCode("Test.B"), "B"), description="desc2", example="example2", values={"A": "A", "B": "B"}, @@ -174,7 +175,7 @@ def test_merge_string_with_string_enum( assert merge_properties(enum_prop, string_prop) == evolve( enum_prop, required=True, - default=Value("'A'" if literal_enums else "test.A", "A"), + default=Value(PythonCode("'A'") if literal_enums else PythonCode("Test.A"), "A"), description=string_prop.description, example=string_prop.example, ) @@ -184,10 +185,10 @@ def test_merge_string_with_string_enum( def test_merge_int_with_int_enum( literal_enums, int_property_factory, enum_property_factory, literal_enum_property_factory ): - int_prop = int_property_factory(default=Value("1", 1), description="desc1", example="example1") + int_prop = int_property_factory(default=Value(PythonCode("1"), 1), description="desc1", example="example1") enum_prop = ( literal_enum_property_factory( - default=Value("1", 1), + default=Value(PythonCode("1"), 1), description="desc2", example="example2", values={1, 2}, @@ -195,7 +196,7 @@ def test_merge_int_with_int_enum( ) if literal_enums else enum_property_factory( - default=Value("test.VALUE_1", 1), + default=Value(PythonCode("Test.VALUE_1"), 1), description="desc2", example="example2", values={"VALUE_1": 1, "VALUE_2": 2}, diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index a51fd984b..b8dc09213 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -1,44 +1,44 @@ -from typing import Optional - import pytest from attr import evolve import openapi_python_client.schema as oai from openapi_python_client.parser.errors import PropertyError -from openapi_python_client.parser.properties import Schemas, StringProperty -from openapi_python_client.parser.properties.model_property import ANY_ADDITIONAL_PROPERTY, _process_properties +from openapi_python_client.parser.properties import Class, ModelProperty, Schemas, StringProperty +from openapi_python_client.parser.properties.model_property import ( + ANY_ADDITIONAL_PROPERTY, + _process_properties, + _PropertyData, + process_model, +) +from openapi_python_client.schema import DataType +from openapi_python_client.schema.ref import Ref +from openapi_python_client.strings import PythonCode MODULE_NAME = "openapi_python_client.parser.properties.model_property" class TestModelProperty: @pytest.mark.parametrize( - "no_optional,required,json,quoted,expected", + "no_optional,required,json,expected", [ - (False, False, False, False, "Union[Unset, MyClass]"), - (False, True, False, False, "MyClass"), - (True, False, False, False, "MyClass"), - (True, True, False, False, "MyClass"), - (False, True, True, False, "dict[str, Any]"), - (False, False, False, True, "Union[Unset, 'MyClass']"), - (False, True, False, True, "'MyClass'"), - (True, False, False, True, "'MyClass'"), - (True, True, False, True, "'MyClass'"), - (False, True, True, True, "dict[str, Any]"), + (False, False, False, "MyClass | Unset"), + (False, True, False, "MyClass"), + (True, False, False, "MyClass"), + (True, True, False, "MyClass"), + (False, True, True, "dict[str, Any]"), ], ) - def test_get_type_string(self, no_optional, required, json, expected, model_property_factory, quoted): + def test_get_type_string(self, no_optional, required, json, expected, model_property_factory): prop = model_property_factory( required=required, ) - assert prop.get_type_string(no_optional=no_optional, json=json, quoted=quoted) == expected + assert prop.get_type_string(no_optional=no_optional, json=json) == PythonCode(expected) def test_get_imports(self, model_property_factory): prop = model_property_factory(required=False) assert prop.get_imports(prefix="..") == { - "from typing import Union", "from ..types import UNSET, Unset", "from typing import cast", } @@ -50,19 +50,9 @@ def test_get_lazy_imports(self, model_property_factory): "from ..models.my_module import MyClass", } - def test_is_base_type(self, model_property_factory): - assert model_property_factory().is_base_type is False - - @pytest.mark.parametrize( - "quoted,expected", - [ - (False, "MyClass"), - (True, '"MyClass"'), - ], - ) - def test_get_base_type_string(self, quoted, expected, model_property_factory): + def test_get_base_type_string(self, model_property_factory): m = model_property_factory() - assert m.get_base_type_string(quoted=quoted) == expected + assert m.get_base_type_string() == PythonCode("MyClass") class TestBuild: @@ -74,7 +64,7 @@ class TestBuild: (None, ANY_ADDITIONAL_PROPERTY), (False, None), ( - oai.Schema.model_construct(type="string"), + oai.Schema(type=DataType.STRING), StringProperty( name="AdditionalProperty", required=True, @@ -87,8 +77,6 @@ class TestBuild: ], ) def test_additional_schemas(self, additional_properties_schema, expected_additional_properties, config): - from openapi_python_client.parser.properties import ModelProperty, Schemas - data = oai.Schema.model_construct( additionalProperties=additional_properties_schema, ) @@ -107,8 +95,6 @@ def test_additional_schemas(self, additional_properties_schema, expected_additio assert model.additional_properties == expected_additional_properties def test_happy_path(self, model_property_factory, string_property_factory, date_time_property_factory, config): - from openapi_python_client.parser.properties import Class, ModelProperty, Schemas - name = "prop" required = True @@ -155,19 +141,15 @@ def test_happy_path(self, model_property_factory, string_property_factory, date_ optional_properties=[date_time_property_factory(name="opt", required=False)], description=data.description, relative_imports={ - "from dateutil.parser import isoparse", "from typing import cast", "import datetime", "from ..types import UNSET, Unset", - "from typing import Union", }, lazy_imports=set(), additional_properties=ANY_ADDITIONAL_PROPERTY, ) def test_model_name_conflict(self, config): - from openapi_python_client.parser.properties import ModelProperty - data = oai.Schema.model_construct() schemas = Schemas(classes_by_name={"OtherModel": None}) @@ -207,14 +189,12 @@ def test_model_name_conflict(self, config): def test_model_naming( self, name: str, - title: Optional[str], - parent_name: Optional[str], + title: str | None, + parent_name: str | None, use_title_prefixing: bool, expected: str, config, ): - from openapi_python_client.parser.properties import ModelProperty - data = oai.Schema( title=title, properties={}, @@ -233,11 +213,9 @@ def test_model_naming( assert result.class_info.name == expected def test_model_bad_properties(self, config): - from openapi_python_client.parser.properties import ModelProperty - data = oai.Schema( properties={ - "bad": oai.Reference.model_construct(ref="#/components/schema/NotExist"), + "bad": oai.Reference.model_construct(ref=Ref("#/components/schema/NotExist")), }, ) result = ModelProperty.build( @@ -253,8 +231,6 @@ def test_model_bad_properties(self, config): assert isinstance(result, PropertyError) def test_model_bad_additional_properties(self, config): - from openapi_python_client.parser.properties import ModelProperty - additional_properties = oai.Schema( type="object", properties={ @@ -275,8 +251,6 @@ def test_model_bad_additional_properties(self, config): assert isinstance(result, PropertyError) def test_process_properties_false(self, model_property_factory, config): - from openapi_python_client.parser.properties import Class, ModelProperty - name = "prop" required = True @@ -327,7 +301,10 @@ def test_conflicting_properties_different_types( self, model_property_factory, string_property_factory, int_property_factory, config ): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) schemas = Schemas( classes_by_reference={ @@ -345,7 +322,7 @@ def test_conflicting_properties_different_types( def test_process_properties_reference_not_exist(self, config): data = oai.Schema( properties={ - "bad": oai.Reference.model_construct(ref="#/components/schema/NotExist"), + "bad": oai.Reference.model_construct(ref=Ref("#/components/schema/NotExist")), }, ) @@ -354,7 +331,9 @@ def test_process_properties_reference_not_exist(self, config): assert isinstance(result, PropertyError) def test_process_properties_all_of_reference_not_exist(self, config): - data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref="#/components/schema/NotExist")]) + data = oai.Schema.model_construct( + allOf=[oai.Reference.model_construct(ref=Ref("#/components/schema/NotExist"))] + ) result = _process_properties(data=data, class_name="", schemas=Schemas(), config=config, roots={"root"}) @@ -369,7 +348,7 @@ def test_process_properties_model_property_roots(self, model_property_factory, c assert all(root in result.optional_props[0].roots for root in roots) def test_invalid_reference(self, config): - data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref="ThisIsNotGood")]) + data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref=Ref("ThisIsNotGood"))]) schemas = Schemas() result = _process_properties(data=data, schemas=schemas, class_name="", config=config, roots={"root"}) @@ -377,7 +356,7 @@ def test_invalid_reference(self, config): assert isinstance(result, PropertyError) def test_non_model_reference(self, enum_property_factory, config): - data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref="#/First")]) + data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref=Ref("#/First"))]) schemas = Schemas( classes_by_reference={ "/First": enum_property_factory(), @@ -389,7 +368,7 @@ def test_non_model_reference(self, enum_property_factory, config): assert isinstance(result, PropertyError) def test_reference_not_processed(self, model_property_factory, config): - data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref="#/Unprocessed")]) + data = oai.Schema.model_construct(allOf=[oai.Reference.model_construct(ref=Ref("#/Unprocessed"))]) schemas = Schemas( classes_by_reference={ "/Unprocessed": model_property_factory(), @@ -404,7 +383,10 @@ def test_allof_string_and_string_enum( self, model_property_factory, enum_property_factory, string_property_factory, config ): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property = enum_property_factory( values={"foo": "foo"}, @@ -426,7 +408,10 @@ def test_allof_string_enum_and_string( self, model_property_factory, enum_property_factory, string_property_factory, config ): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property = enum_property_factory( required=False, @@ -447,7 +432,10 @@ def test_allof_string_enum_and_string( def test_allof_int_and_int_enum(self, model_property_factory, enum_property_factory, int_property_factory, config): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property = enum_property_factory( values={"foo": 1}, @@ -467,7 +455,10 @@ def test_allof_enum_incompatible_type( self, model_property_factory, enum_property_factory, int_property_factory, config ): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property = enum_property_factory( values={"foo": 1}, @@ -485,7 +476,10 @@ def test_allof_enum_incompatible_type( def test_allof_string_enums(self, model_property_factory, enum_property_factory, config): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property1 = enum_property_factory( name="an_enum", @@ -509,7 +503,10 @@ def test_allof_string_enums(self, model_property_factory, enum_property_factory, def test_allof_int_enums(self, model_property_factory, enum_property_factory, config): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property1 = enum_property_factory( name="an_enum", @@ -533,7 +530,10 @@ def test_allof_int_enums(self, model_property_factory, enum_property_factory, co def test_allof_enums_are_not_subsets(self, model_property_factory, enum_property_factory, config): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) enum_property1 = enum_property_factory( name="an_enum", @@ -557,7 +557,10 @@ def test_allof_enums_are_not_subsets(self, model_property_factory, enum_property def test_duplicate_properties(self, model_property_factory, string_property_factory, config): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) prop = string_property_factory(required=False) schemas = Schemas( @@ -571,6 +574,42 @@ def test_duplicate_properties(self, model_property_factory, string_property_fact assert result.optional_props == [prop], "There should only be one copy of duplicate properties" + def test_allof_required_override(self, model_property_factory, string_property_factory, config): + """Test that required field can be overridden in allOf schemas""" + # Simulates: + # FooBase: + # type: object + # properties: + # bar: {type: string} + # baz: {type: string} + # FooCreate: + # allOf: + # - $ref: '#/components/schemas/FooBase' + # - type: object + # required: [bar] + bar_prop = string_property_factory(name="bar", required=False) + baz_prop = string_property_factory(name="baz", required=False) + + data = oai.Schema.model_construct( + allOf=[ + oai.Reference.model_construct(ref=Ref("#/FooBase")), + oai.Schema.model_construct(type="object", required=["bar"]), + ] + ) + schemas = Schemas( + classes_by_reference={ + "/FooBase": model_property_factory(required_properties=[], optional_properties=[bar_prop, baz_prop]), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="FooCreate", config=config, roots={"root"}) + + # bar should now be required, baz should remain optional + assert len(result.required_props) == 1 + assert result.required_props[0].name == "bar" + assert len(result.optional_props) == 1 + assert result.optional_props[0].name == "baz" + @pytest.mark.parametrize("first_required", [True, False]) @pytest.mark.parametrize("second_required", [True, False]) def test_mixed_requirements( @@ -582,7 +621,10 @@ def test_mixed_requirements( config, ): data = oai.Schema.model_construct( - allOf=[oai.Reference.model_construct(ref="#/First"), oai.Reference.model_construct(ref="#/Second")] + allOf=[ + oai.Reference.model_construct(ref=Ref("#/First")), + oai.Reference.model_construct(ref=Ref("#/Second")), + ] ) schemas = Schemas( classes_by_reference={ @@ -671,9 +713,6 @@ def test_merge_inline_objects(self, model_property_factory, enum_property_factor class TestProcessModel: def test_process_model_error(self, mocker, model_property_factory, config): - from openapi_python_client.parser.properties import Schemas - from openapi_python_client.parser.properties.model_property import process_model - model_prop = model_property_factory() schemas = Schemas() process_property_data = mocker.patch(f"{MODULE_NAME}._process_property_data") @@ -688,9 +727,6 @@ def test_process_model_error(self, mocker, model_property_factory, config): assert model_prop.additional_properties is None def test_process_model(self, mocker, model_property_factory, config): - from openapi_python_client.parser.properties import Schemas - from openapi_python_client.parser.properties.model_property import _PropertyData, process_model - model_prop = model_property_factory() schemas = Schemas() property_data = _PropertyData( @@ -715,8 +751,6 @@ def test_process_model(self, mocker, model_property_factory, config): def test_set_relative_imports(model_property_factory): - from openapi_python_client.parser.properties import Class - class_info = Class("ClassName", module_name="module_name") relative_imports = {f"from ..models.{class_info.module_name} import {class_info.name}"} diff --git a/tests/test_parser/test_properties/test_none.py b/tests/test_parser/test_properties/test_none.py index d61ca0136..760f6c702 100644 --- a/tests/test_parser/test_properties/test_none.py +++ b/tests/test_parser/test_properties/test_none.py @@ -1,10 +1,12 @@ from openapi_python_client.parser.errors import PropertyError from openapi_python_client.parser.properties import NoneProperty from openapi_python_client.parser.properties.protocol import Value -from openapi_python_client.utils import PythonIdentifier +from openapi_python_client.strings import PythonIdentifier def test_default(): + # currently this is testing an unused code path: + # https://github.com/openapi-generators/openapi-python-client/issues/1162 err = NoneProperty.build( default="not None", description=None, diff --git a/tests/test_parser/test_properties/test_protocol.py b/tests/test_parser/test_properties/test_protocol.py index 1d4111750..c69aa955b 100644 --- a/tests/test_parser/test_properties/test_protocol.py +++ b/tests/test_parser/test_properties/test_protocol.py @@ -2,51 +2,48 @@ import pytest -from openapi_python_client.parser.properties.protocol import Value - - -def test_is_base_type(any_property_factory): - assert any_property_factory().is_base_type is True +from openapi_python_client.parser.properties import AnyProperty +from openapi_python_client.parser.properties.protocol import Value, convert_example +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import PythonCode @pytest.mark.parametrize( - "required,no_optional,json,quoted,expected", + "required,no_optional,json,expected", [ - (False, False, False, False, "Union[Unset, TestType]"), - (False, True, False, False, "TestType"), - (True, False, False, False, "TestType"), - (True, True, False, False, "TestType"), - (False, False, True, False, "Union[Unset, str]"), - (False, True, True, False, "str"), - (True, False, True, False, "str"), - (True, True, True, False, "str"), + (False, False, False, "TestType | Unset"), + (False, True, False, "TestType"), + (True, False, False, "TestType"), + (True, True, False, "TestType"), + (False, False, True, "str | Unset"), + (False, True, True, "str"), + (True, False, True, "str"), + (True, True, True, "str"), ], ) -def test_get_type_string(any_property_factory, mocker, required, no_optional, json, expected, quoted): - from openapi_python_client.parser.properties import AnyProperty - +def test_get_type_string(any_property_factory, mocker, required, no_optional, json, expected): mocker.patch.object(AnyProperty, "_type_string", "TestType") mocker.patch.object(AnyProperty, "_json_type_string", "str") p = any_property_factory(required=required) - assert p.get_type_string(no_optional=no_optional, json=json, quoted=quoted) == expected + assert p.get_type_string(no_optional=no_optional, json=json) == PythonCode(expected) @pytest.mark.parametrize( "default,required,expected", [ - (None, False, "test: Union[Unset, Any] = UNSET"), + (None, False, "test: Any | Unset = UNSET"), (None, True, "test: Any"), - ("Test", False, "test: Union[Unset, Any] = Test"), + ("Test", False, "test: Any | Unset = Test"), ("Test", True, "test: Any = Test"), ], ) def test_to_string(default: str | None, required: bool, expected: str, any_property_factory): name = "test" p = any_property_factory( - name=name, required=required, default=Value(default, default) if default is not None else None + name=name, required=required, default=Value(PythonCode(default), default) if default is not None else None ) - assert p.to_string() == expected + assert p.to_string() == PythonCode(expected) def test_get_imports(any_property_factory): @@ -54,34 +51,67 @@ def test_get_imports(any_property_factory): assert p.get_imports(prefix="") == set() p = any_property_factory(name="test", required=False, default=None) - assert p.get_imports(prefix="") == {"from types import UNSET, Unset", "from typing import Union"} + assert p.get_imports(prefix="") == {"from types import UNSET, Unset"} -@pytest.mark.parametrize( - "quoted,expected", - [ - (False, "TestType"), - (True, "TestType"), - ], -) -def test_get_base_type_string(quoted, expected, any_property_factory, mocker): - from openapi_python_client.parser.properties import AnyProperty +def test_to_docstring_escapes_description_and_example(any_property_factory): + """Descriptions and examples must not be able to break out of the docstring""" + p = any_property_factory( + description='Break out """ print("uh oh")', + example='Example """ print("uh oh")', + ) - mocker.patch.object(AnyProperty, "_type_string", "TestType") - p = any_property_factory() - assert p.get_base_type_string(quoted=quoted) is expected + doc = p.to_docstring() + assert '"""' not in doc + assert r'\"\"\" print("uh oh")' in doc -@pytest.mark.parametrize( - "quoted,expected", - [ - (False, "str"), - (True, "str"), - ], -) -def test_get_base_json_type_string(quoted, expected, any_property_factory, mocker): - from openapi_python_client.parser.properties import AnyProperty - mocker.patch.object(AnyProperty, "_json_type_string", "str") +def test_to_docstring_escapes_type_string(any_property_factory, mocker): + """A type string containing literal values (e.g. const) must not break out of the docstring""" + mocker.patch.object(AnyProperty, "_type_string", 'Literal[\'"""\']') p = any_property_factory() - assert p.get_base_json_type_string(quoted=quoted) is expected + + doc = p.to_docstring() + + assert '"""' not in doc + + +def test_to_docstring_escapes_default(any_property_factory): + """A default's python_code must not be able to break out of the docstring""" + # python_code for the string default '"""' is the 5-char source: "\"\"\"" + p = any_property_factory(default=Value(python_code=PythonCode('"\\"\\"\\""'), raw_value='"""')) + + doc = p.to_docstring() + + # The raw triple-quote must not survive into the docstring text + assert '"""' not in doc + + +def test_get_instance_type_string(any_property_factory): + p = any_property_factory(required=False) + assert p.get_instance_type_string() == PythonCode("Any") + + +class TestConvertExample: + def test_none(self): + assert convert_example(None) is None + + def test_untrusted_string_passes_through(self): + example = UntrustedString("an example") + assert convert_example(example) is example + + def test_string_is_wrapped(self): + converted = convert_example("an example") + assert isinstance(converted, UntrustedString) + assert converted == "an example" + + def test_non_string_is_stringified(self): + """OpenAPI examples can be any type; they only appear in docstrings so stringify them""" + converted = convert_example({"a": 1}) + assert isinstance(converted, UntrustedString) + assert converted == "{'a': 1}" + + def test_property_converts_example(self, any_property_factory): + p = any_property_factory(example={"a": 1}) + assert p.example == "{'a': 1}" diff --git a/tests/test_parser/test_properties/test_schemas.py b/tests/test_parser/test_properties/test_schemas.py index 5560795cf..8433404d2 100644 --- a/tests/test_parser/test_properties/test_schemas.py +++ b/tests/test_parser/test_properties/test_schemas.py @@ -1,18 +1,23 @@ import pytest from attr import evolve +from openapi_python_client.config import ClassOverride from openapi_python_client.parser.errors import ParameterError from openapi_python_client.parser.properties import Class, Parameters -from openapi_python_client.parser.properties.schemas import parameter_from_reference -from openapi_python_client.schema import Parameter, Reference -from openapi_python_client.utils import ClassName +from openapi_python_client.parser.properties.schemas import ( + ReferencePath, + parameter_from_data, + parameter_from_reference, +) +from openapi_python_client.schema import Parameter, ParameterLocation, Reference, Schema +from openapi_python_client.schema.ref import Ref +from openapi_python_client.schema.untrusted_string import UntrustedString +from openapi_python_client.strings import ClassName MODULE_NAME = "openapi_python_client.parser.properties.schemas" def test_class_from_string_default_config(config): - from openapi_python_client.parser.properties import Class - class_ = Class.from_string(string="#/components/schemas/PingResponse", config=config) assert class_.name == "PingResponse" @@ -29,9 +34,6 @@ def test_class_from_string_default_config(config): ), ) def test_class_from_string(class_override, module_override, expected_class, expected_module, config): - from openapi_python_client.config import ClassOverride - from openapi_python_client.parser.properties import Class - ref = "#/components/schemas/MyResponse" config = evolve( config, class_overrides={"MyResponse": ClassOverride(class_name=class_override, module_name=module_override)} @@ -44,9 +46,6 @@ def test_class_from_string(class_override, module_override, expected_class, expe class TestParameterFromData: def test_cannot_parse_parameters_by_reference(self, config): - from openapi_python_client.parser.properties import Parameters - from openapi_python_client.parser.properties.schemas import parameter_from_data - ref = Reference.model_construct(ref="#/components/parameters/a_param") parameters = Parameters() param_or_error, new_parameters = parameter_from_data( @@ -56,10 +55,6 @@ def test_cannot_parse_parameters_by_reference(self, config): assert new_parameters == parameters def test_parameters_without_schema_are_ignored(self, config): - from openapi_python_client.parser.properties import Parameters - from openapi_python_client.parser.properties.schemas import parameter_from_data - from openapi_python_client.schema import ParameterLocation - param = Parameter(name="a_schemaless_param", param_in=ParameterLocation.QUERY) parameters = Parameters() param_or_error, new_parameters = parameter_from_data( @@ -69,12 +64,8 @@ def test_parameters_without_schema_are_ignored(self, config): assert new_parameters == parameters def test_registers_new_parameters(self, config): - from openapi_python_client.parser.properties import Parameters - from openapi_python_client.parser.properties.schemas import parameter_from_data - from openapi_python_client.schema import ParameterLocation, Schema - param = Parameter.model_construct( - name="a_param", param_in=ParameterLocation.QUERY, param_schema=Schema.model_construct() + name=UntrustedString("a_param"), param_in=ParameterLocation.QUERY, param_schema=Schema.model_construct() ) parameters = Parameters() param_or_error, new_parameters = parameter_from_data( @@ -92,10 +83,10 @@ def test_returns_parameter_if_parameter_provided(self): assert param_or_error == param def test_errors_out_if_reference_not_in_parameters(self): - ref = Reference.model_construct(ref="#/components/parameters/a_param") - class_info = Class(name="a_param", module_name="module_name") + ref = Reference.model_construct(ref=Ref("#/components/parameters/a_param")) + class_info = Class(name=ClassName(UntrustedString("a_param"), prefix=""), module_name="module_name") existing_param = Parameter.model_construct(name="a_param") - param_by_ref = Reference.model_construct(ref="#/components/parameters/another_param") + param_by_ref = Reference.model_construct(ref=Ref("#/components/parameters/another_param")) params = Parameters( classes_by_name={class_info.name: existing_param}, classes_by_reference={ref.ref: existing_param} ) @@ -106,53 +97,12 @@ def test_errors_out_if_reference_not_in_parameters(self): def test_returns_reference_from_registry(self): existing_param = Parameter.model_construct(name="a_param") - class_info = Class(name="MyParameter", module_name="module_name") + class_info = Class(name=ClassName(UntrustedString("MyParameter"), prefix=""), module_name="module_name") params = Parameters( classes_by_name={class_info.name: existing_param}, - classes_by_reference={"/components/parameters/a_param": existing_param}, + classes_by_reference={ReferencePath(UntrustedString("/components/parameters/a_param")): existing_param}, ) - param_by_ref = Reference.model_construct(ref="#/components/parameters/a_param") + param_by_ref = Reference.model_construct(ref=Ref("#/components/parameters/a_param")) param_or_error = parameter_from_reference(param=param_by_ref, parameters=params) assert param_or_error == existing_param - - -class TestUpdateParametersFromData: - def test_reports_parameters_with_errors(self, mocker, config): - from openapi_python_client.parser.properties.schemas import update_parameters_with_data - from openapi_python_client.schema import ParameterLocation, Schema - - parameters = Parameters() - param = Parameter.model_construct( - name="a_param", param_in=ParameterLocation.QUERY, param_schema=Schema.model_construct() - ) - parameter_from_data = mocker.patch( - f"{MODULE_NAME}.parameter_from_data", side_effect=[(ParameterError(), parameters)] - ) - ref_path = Reference.model_construct(ref="#/components/parameters/a_param") - new_parameters_or_error = update_parameters_with_data( - ref_path=ref_path.ref, data=param, parameters=parameters, config=config - ) - - parameter_from_data.assert_called_once() - assert new_parameters_or_error == ParameterError( - detail="Unable to parse this part of your OpenAPI document: : None", - header="Unable to parse parameter #/components/parameters/a_param", - ) - - def test_records_references_to_parameters(self, mocker, config): - from openapi_python_client.parser.properties.schemas import update_parameters_with_data - from openapi_python_client.schema import ParameterLocation, Schema - - parameters = Parameters() - param = Parameter.model_construct( - name="a_param", param_in=ParameterLocation.QUERY, param_schema=Schema.model_construct() - ) - parameter_from_data = mocker.patch(f"{MODULE_NAME}.parameter_from_data", side_effect=[(param, parameters)]) - ref_path = "#/components/parameters/a_param" - new_parameters = update_parameters_with_data( - ref_path=ref_path, data=param, parameters=parameters, config=config - ) - - parameter_from_data.assert_called_once() - assert new_parameters.classes_by_reference[ref_path] == param diff --git a/tests/test_parser/test_properties/test_union.py b/tests/test_parser/test_properties/test_union.py index acbbd06d6..b86100b69 100644 --- a/tests/test_parser/test_properties/test_union.py +++ b/tests/test_parser/test_properties/test_union.py @@ -1,70 +1,17 @@ import openapi_python_client.schema as oai -from openapi_python_client.parser.errors import ParseError, PropertyError -from openapi_python_client.parser.properties import Schemas, UnionProperty -from openapi_python_client.parser.properties.protocol import Value +from openapi_python_client import UntrustedString +from openapi_python_client.parser.errors import ParseError +from openapi_python_client.parser.properties import Schemas, UnionProperty, property_from_data from openapi_python_client.schema import DataType, ParameterLocation -def test_property_from_data_union(union_property_factory, date_time_property_factory, string_property_factory, config): - from openapi_python_client.parser.properties import Schemas, property_from_data - - name = "union_prop" - required = True - data = oai.Schema( - anyOf=[oai.Schema(type=DataType.STRING, default="a")], - oneOf=[ - oai.Schema(type=DataType.STRING, schema_format="date-time"), - ], - ) - expected = union_property_factory( - name=name, - required=required, - inner_properties=[ - string_property_factory(name=f"{name}_type_0", default=Value("'a'", "a")), - date_time_property_factory(name=f"{name}_type_1"), - ], - ) - - p, s = property_from_data( - name=name, required=required, data=data, schemas=Schemas(), parent_name="parent", config=config - ) - - assert p == expected - assert s == Schemas() - - -def test_build_union_property_invalid_property(config): - name = "bad_union" - required = True - reference = oai.Reference.model_construct(ref="#/components/schema/NotExist") - data = oai.Schema(anyOf=[reference]) - - p, s = UnionProperty.build( - name=name, required=required, data=data, schemas=Schemas(), parent_name="parent", config=config - ) - assert p == PropertyError(detail=f"Invalid property in union {name}", data=reference) - - -def test_invalid_default(config): - data = oai.Schema( - type=[DataType.NUMBER, DataType.INTEGER], - default="a", - ) - - err, _ = UnionProperty.build( - data=data, required=True, schemas=Schemas(), parent_name="parent", name="name", config=config - ) - - assert isinstance(err, PropertyError) - - def test_invalid_location(config): data = oai.Schema( type=[DataType.NUMBER, DataType.NULL], ) prop, _ = UnionProperty.build( - data=data, required=True, schemas=Schemas(), parent_name="parent", name="name", config=config + data=data, required=True, schemas=Schemas(), parent_name="parent", name=UntrustedString("name"), config=config ) err = prop.validate_location(ParameterLocation.PATH) @@ -77,8 +24,60 @@ def test_not_required_in_path(config): ) prop, _ = UnionProperty.build( - data=data, required=False, schemas=Schemas(), parent_name="parent", name="name", config=config + data=data, required=False, schemas=Schemas(), parent_name="parent", name=UntrustedString("name"), config=config ) err = prop.validate_location(ParameterLocation.PATH) assert isinstance(err, ParseError) + + +def test_union_oneOf_descriptive_type_name( + union_property_factory, + date_time_property_factory, + string_property_factory, + boolean_property_factory, + date_property_factory, + int_property_factory, + float_property_factory, + config, +): + nested_schema_variant_A = oai.Schema(type=DataType.STRING, title="A") + nested_schema_variant_B = oai.Schema(type=DataType.INTEGER, title="B") + nested_schema_variant_2 = oai.Schema(type=DataType.NUMBER) + nested_schema_variant_C = oai.Schema(type=DataType.BOOLEAN, title="C") + + name = "union_prop" + required = True + data = oai.Schema( + anyOf=[ + # AnyOf retains the old naming convention + nested_schema_variant_C, + oai.Schema(type=DataType.STRING, schema_format="date"), + ], + oneOf=[ + # OneOf fields that define their own titles will have those titles as their Type names + nested_schema_variant_A, + nested_schema_variant_B, + nested_schema_variant_2, + oai.Schema(type=DataType.STRING, schema_format="date-time"), + ], + ) + expected = union_property_factory( + name=name, + required=required, + inner_properties=[ + boolean_property_factory(name=f"{name}_C"), + date_property_factory(name=f"{name}_type_1"), + string_property_factory(name=f"{name}_A"), + int_property_factory(name=f"{name}_B"), + float_property_factory(name=f"{name}_type_4"), + date_time_property_factory(name=f"{name}_type_5"), + ], + ) + + p, s = property_from_data( + name=UntrustedString(name), required=required, data=data, schemas=Schemas(), parent_name="parent", config=config + ) + + assert p == expected + assert s == Schemas() diff --git a/tests/test_parser/test_responses.py b/tests/test_parser/test_responses.py index 0ac885764..70cbd88d3 100644 --- a/tests/test_parser/test_responses.py +++ b/tests/test_parser/test_responses.py @@ -1,28 +1,37 @@ from unittest.mock import MagicMock +import pytest + import openapi_python_client.schema as oai +from openapi_python_client import UntrustedString +from openapi_python_client.parser import responses from openapi_python_client.parser.errors import ParseError, PropertyError from openapi_python_client.parser.properties import Schemas -from openapi_python_client.parser.responses import JSON_SOURCE, NONE_SOURCE +from openapi_python_client.parser.responses import ( + JSON_SOURCE, + NONE_SOURCE, + HTTPStatusPattern, + Response, + response_from_data, +) MODULE_NAME = "openapi_python_client.parser.responses" def test_response_from_data_no_content(any_property_factory): - from openapi_python_client.parser.responses import Response, response_from_data - data = oai.Response.model_construct(description="") - response, schemas = response_from_data( - status_code=200, + response, _schemas = response_from_data( + status_code=HTTPStatusPattern(pattern="200", code_range=(200, 200)), data=data, schemas=Schemas(), + responses={}, parent_name="parent", config=MagicMock(), ) assert response == Response( - status_code=200, + status_code=HTTPStatusPattern(pattern="200", code_range=(200, 200)), prop=any_property_factory( name="response_200", default=None, @@ -34,67 +43,43 @@ def test_response_from_data_no_content(any_property_factory): ) -def test_response_from_data_reference(any_property_factory): - from openapi_python_client.parser.responses import Response, response_from_data - - data = oai.Reference.model_construct() - - response, schemas = response_from_data( - status_code=200, - data=data, - schemas=Schemas(), - parent_name="parent", - config=MagicMock(), - ) - - assert response == Response( - status_code=200, - prop=any_property_factory( - name="response_200", - default=None, - required=True, - ), - source=NONE_SOURCE, - data=data, - ) +status_code = HTTPStatusPattern(pattern="200", code_range=(200, 200)) def test_response_from_data_unsupported_content_type(): - from openapi_python_client.parser.responses import response_from_data - - data = oai.Response.model_construct(description="", content={"blah": None}) + data = oai.Response.model_construct(description="", content={oai.UntrustedString("blah"): None}) config = MagicMock() config.content_type_overrides = {} - response, schemas = response_from_data( - status_code=200, + response, _schemas = response_from_data( + status_code=status_code, data=data, schemas=Schemas(), + responses={}, parent_name="parent", config=config, ) - assert response == ParseError(data=data, detail="Unsupported content_type {'blah': None}") + assert response == ParseError(data=data, detail="Unsupported content_type blah") def test_response_from_data_no_content_schema(any_property_factory): - from openapi_python_client.parser.responses import Response, response_from_data - data = oai.Response.model_construct( description="", - content={"application/vnd.api+json; version=2.2": oai.MediaType.model_construct()}, + content={oai.UntrustedString("application/vnd.api+json; version=2.2"): oai.MediaType.model_construct()}, ) config = MagicMock() config.content_type_overrides = {} - response, schemas = response_from_data( - status_code=200, + response, _schemas = response_from_data( + status_code=status_code, data=data, schemas=Schemas(), + responses={}, parent_name="parent", config=config, ) assert response == Response( - status_code=200, + status_code=status_code, prop=any_property_factory( name="response_200", default=None, @@ -107,20 +92,19 @@ def test_response_from_data_no_content_schema(any_property_factory): def test_response_from_data_property_error(mocker): - from openapi_python_client.parser import responses - property_from_data = mocker.patch.object(responses, "property_from_data", return_value=(PropertyError(), Schemas())) data = oai.Response.model_construct( description="", - content={"application/json": oai.MediaType.model_construct(media_type_schema="something")}, + content={oai.UntrustedString("application/json"): oai.MediaType.model_construct(media_type_schema="something")}, ) config = MagicMock() config.content_type_overrides = {} - response, schemas = responses.response_from_data( - status_code=400, + response, _schemas = responses.response_from_data( + status_code=HTTPStatusPattern(pattern="400", code_range=(400, 400)), data=data, schemas=Schemas(), + responses={}, parent_name="parent", config=config, ) @@ -137,27 +121,27 @@ def test_response_from_data_property_error(mocker): def test_response_from_data_property(mocker, any_property_factory): - from openapi_python_client.parser import responses - prop = any_property_factory() property_from_data = mocker.patch.object(responses, "property_from_data", return_value=(prop, Schemas())) data = oai.Response.model_construct( description="", - content={"application/json": oai.MediaType.model_construct(media_type_schema="something")}, + content={oai.UntrustedString("application/json"): oai.MediaType.model_construct(media_type_schema="something")}, ) config = MagicMock() config.content_type_overrides = {} + status_code = HTTPStatusPattern(pattern="400", code_range=(400, 400)) - response, schemas = responses.response_from_data( - status_code=400, + response, _schemas = responses.response_from_data( + status_code=status_code, data=data, schemas=Schemas(), + responses={}, parent_name="parent", config=config, ) assert response == responses.Response( - status_code=400, + status_code=status_code, prop=prop, source=JSON_SOURCE, data=data, @@ -173,24 +157,23 @@ def test_response_from_data_property(mocker, any_property_factory): def test_response_from_data_content_type_overrides(any_property_factory): - from openapi_python_client.parser.responses import Response, response_from_data - data = oai.Response.model_construct( description="", - content={"application/zip": oai.MediaType.model_construct()}, + content={oai.UntrustedString("application/zip"): oai.MediaType.model_construct()}, ) config = MagicMock() config.content_type_overrides = {"application/zip": "application/octet-stream"} - response, schemas = response_from_data( - status_code=200, + response, _schemas = response_from_data( + status_code=HTTPStatusPattern(pattern="200", code_range=(200, 200)), data=data, schemas=Schemas(), + responses={}, parent_name="parent", config=config, ) assert response == Response( - status_code=200, + status_code=HTTPStatusPattern(pattern="200", code_range=(200, 200)), prop=any_property_factory( name="response_200", default=None, @@ -200,3 +183,23 @@ def test_response_from_data_content_type_overrides(any_property_factory): source=NONE_SOURCE, data=data, ) + + +@pytest.mark.parametrize( + "pattern1, pattern2, result", + [ + ("400", "401", True), + ("503", "500", False), + ("default", "400", False), + ("400", "default", True), + ("2XX", "3XX", True), + ("3XX", "2XX", False), + ("2XX", "400", False), + ], +) +def test_http_status_pattern_lt(pattern1: str, pattern2: str, result: bool) -> None: + first = HTTPStatusPattern.parse(UntrustedString(pattern1)) + second = HTTPStatusPattern.parse(UntrustedString(pattern2)) + assert isinstance(first, HTTPStatusPattern) + assert isinstance(second, HTTPStatusPattern) + assert (first < second) == result diff --git a/tests/test_schema/test_data_type.py b/tests/test_schema/test_data_type.py index 19aa256eb..0a4d9681d 100644 --- a/tests/test_schema/test_data_type.py +++ b/tests/test_schema/test_data_type.py @@ -1,3 +1,4 @@ +import pydantic import pytest import openapi_python_client.schema as oai @@ -5,8 +6,6 @@ class TestDataType: def test_schema_bad_types(self): - import pydantic - with pytest.raises(pydantic.ValidationError): oai.Schema(type="bad_type") diff --git a/tests/test_schema/test_noisy_refs.py b/tests/test_schema/test_noisy_refs.py new file mode 100644 index 000000000..ff5a7cf1c --- /dev/null +++ b/tests/test_schema/test_noisy_refs.py @@ -0,0 +1,89 @@ +# If a field may be reference (`Union[Reference, OtherType]`) and the dictionary +# being processed for it contains "$ref", it seems like it should preferentially +# be parsed as a `Reference`[1]. Since the models are defined with +# `extra="allow"`, Pydantic won't guarantee this parse if the dictionary is in +# an unspecified sense a "better match" for `OtherType`[2], e.g., perhaps if it +# has several more fields matching that type versus the single match for `$ref`. +# +# We can use a discriminated union to force parsing these dictionaries as +# `Reference`s. +# +# References: +# [1] https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object +# [2] https://docs.pydantic.dev/latest/concepts/unions/#smart-mode +from typing import Annotated, TypeVar, get_args, get_origin + +import pytest +from pydantic import TypeAdapter + +from openapi_python_client.schema.openapi_schema_pydantic import ( + Callback, + Example, + Header, + Link, + Parameter, + PathItem, + Reference, + RequestBody, + Response, + Schema, + SecurityScheme, +) + +try: + from openapi_python_client.schema.openapi_schema_pydantic.reference import ReferenceOr +except ImportError: + T = TypeVar("T") + ReferenceOr = Reference | T + + +def get_example(base_type): + schema = base_type.model_json_schema() + if "examples" in schema: + return schema["examples"][0] + if "$defs" in schema: + return schema["$defs"][base_type.__name__]["examples"][0] + raise TypeError(f"No example found for {base_type.__name__}") + + +def deannotate_type(t): + while get_origin(t) is Annotated: + t = get_args(t)[0] + return t + + +# The following types occur in various models, so we want to make sure they +# parse properly. They are verified to /fail/ to parse as of commit 3bd12f86. + + +@pytest.mark.parametrize( + ("ref_or_type", "get_example_fn"), + [ + (ReferenceOr[Callback], lambda t: {"test1": get_example(PathItem), "test2": get_example(PathItem)}), + (ReferenceOr[Example], get_example), + (ReferenceOr[Header], get_example), + (ReferenceOr[Link], get_example), + (ReferenceOr[Parameter], get_example), + (ReferenceOr[RequestBody], get_example), + (ReferenceOr[Response], get_example), + (ReferenceOr[Schema], get_example), + (ReferenceOr[SecurityScheme], get_example), + ], +) +def test_type(ref_or_type, get_example_fn): + base_type = None + for maybe_annotated_type in get_args(deannotate_type(ref_or_type)): + each_type = deannotate_type(maybe_annotated_type) + if each_type is not Reference: + base_type = each_type + break + assert base_type is not None + + example = get_example_fn(base_type) + + parsed = TypeAdapter(ref_or_type).validate_python(example) + assert type(parsed) is get_origin(base_type) or base_type + + example["$ref"] = "ref" + parsed = TypeAdapter(ref_or_type).validate_python(example) + assert type(parsed) is Reference diff --git a/tests/test_schema/test_open_api.py b/tests/test_schema/test_open_api.py index bdab19eba..02b4946c1 100644 --- a/tests/test_schema/test_open_api.py +++ b/tests/test_schema/test_open_api.py @@ -2,6 +2,7 @@ from pydantic import ValidationError from openapi_python_client.schema import OpenAPI +from openapi_python_client.schema.untrusted_string import UntrustedString @pytest.mark.parametrize( @@ -40,6 +41,6 @@ def test_parse_with_callback(): } open_api = OpenAPI.model_validate(data) - create_endpoint = open_api.paths["/create"] + create_endpoint = open_api.paths[UntrustedString("/create")] assert "200" in create_endpoint.post.responses assert "200" in create_endpoint.post.callbacks["event"]["callback"].post.responses diff --git a/tests/test_schema/test_untrusted_string.py b/tests/test_schema/test_untrusted_string.py new file mode 100644 index 000000000..54350daf1 --- /dev/null +++ b/tests/test_schema/test_untrusted_string.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel + +from openapi_python_client.schema.ref import Ref +from openapi_python_client.schema.untrusted_string import UntrustedString + + +class Model(BaseModel): + untrusted: UntrustedString + + +class RefModel(BaseModel): + ref: Ref + + +def test_ref_json_serialization(): + """Ref must serialize to its raw value in JSON mode so error messages can include it.""" + m = RefModel.model_validate({"ref": "#/components/schemas/Foo"}) + assert m.model_dump_json() == '{"ref":"#/components/schemas/Foo"}' + + +def test_untrusted_string_deserialization(): + m = Model.model_validate_json('{"untrusted":"hello"}') + assert m.untrusted.get_untrusted_value() == "hello" + + +def test_untrusted_string_json_serialization(): + """UntrustedString must serialize to its raw value in JSON mode so error messages can include it.""" + m = Model.model_validate_json('{"untrusted":"hello"}') + assert m.model_dump_json() == '{"untrusted":"hello"}' + # Python mode keeps the wrapper intact so it can't leak into generated code + assert isinstance(m.model_dump()["untrusted"], UntrustedString) diff --git a/tests/test_strings.py b/tests/test_strings.py new file mode 100644 index 000000000..a90fb6e2a --- /dev/null +++ b/tests/test_strings.py @@ -0,0 +1,328 @@ +import ast +import copy +import textwrap +import warnings + +import pytest + +from openapi_python_client import strings +from openapi_python_client.schema.untrusted_string import UntrustedString + + +class TestPythonIdentifier: + def test_valid_identifier_is_not_changed(self): + assert strings.PythonIdentifier(value="valid_field", prefix="field") == "valid_field" + + def test_numbers_are_prefixed(self): + assert strings.PythonIdentifier(value="1", prefix="field") == "field1" + + def test_invalid_symbols_are_stripped(self): + assert strings.PythonIdentifier(value="$abc", prefix="prefix") == "abc" + + def test_keywords_are_postfixed(self): + assert strings.PythonIdentifier(value="for", prefix="prefix") == "for_" + + def test_empty_is_prefixed(self): + assert strings.PythonIdentifier(value="", prefix="something") == "something" + + def test_skip_snake_case_preserves_case(self): + assert strings.PythonIdentifier(value="fooBar", prefix="field", skip_snake_case=True) == "fooBar" + + def test_skip_snake_case_strips_delimiters(self): + assert strings.PythonIdentifier(value="foo-bar", prefix="field", skip_snake_case=True) == "foobar" + assert strings.PythonIdentifier(value="foo bar", prefix="field", skip_snake_case=True) == "foobar" + assert strings.PythonIdentifier(value="foo.bar", prefix="field", skip_snake_case=True) == "foobar" + + def test_skip_snake_case_keeps_underscores(self): + assert strings.PythonIdentifier(value="foo_bar", prefix="field", skip_snake_case=True) == "foo_bar" + + def test_skip_snake_case_result_is_always_valid_identifier(self): + result = strings.PythonIdentifier(value="foo-bar", prefix="field", skip_snake_case=True) + assert result.isidentifier() + assert strings.PythonIdentifier(value="1-2", prefix="field", skip_snake_case=True).isidentifier() + assert strings.PythonIdentifier(value="---", prefix="field", skip_snake_case=True).isidentifier() + + def test_non_identifier_unicode_is_stripped(self): + # U+00BC VULGAR FRACTION ONE QUARTER matches \w but is not valid in identifiers + assert strings.PythonIdentifier(value="¼", prefix="field") == "field" + assert strings.PythonIdentifier(value="¼", prefix="field").isidentifier() + + +class TestClassName: + def test_valid_is_not_changed(self): + assert strings.ClassName(value=UntrustedString("ValidClass"), prefix="field") == "ValidClass" + + def test_numbers_are_prefixed(self): + assert strings.ClassName(value=UntrustedString("1"), prefix="field") == "Field1" + + def test_invalid_symbols_are_stripped(self): + assert strings.ClassName(value=UntrustedString("$abc"), prefix="prefix") == "Abc" + + def test_keywords_are_postfixed(self): + assert strings.ClassName(value=UntrustedString("none"), prefix="prefix") == "None_" + + def test_empty_is_prefixed(self): + assert strings.ClassName(value=UntrustedString(""), prefix="something") == "Something" + + def test_deepcopy_returns_self(self): + name = strings.ClassName(value=UntrustedString("ValidClass"), prefix="field") + assert copy.deepcopy(name) is name + + +@pytest.mark.parametrize( + "before, after", + [ + ("connectionID", ["connection", "ID"]), + ("connection_id", ["connection", "id"]), + ("connection-id", ["connection", "id"]), + ("Response200", ["Response", "200"]), + ("Response200Okay", ["Response", "200", "Okay"]), + ("S3Config", ["S3", "Config"]), + ("s3config", ["s3config"]), + ("fully.qualified.Name", ["fully", "qualified", "Name"]), + ], +) +def test_split_words(before, after): + assert strings.split_words(before) == after + + +def test_snake_case_uppercase_str(): + assert strings.snake_case("HTTP") == "http" + assert strings.snake_case("HTTP RESPONSE") == "http_response" + + +def test_snake_case_from_pascal_with_acronyms(): + assert strings.snake_case("HTTPResponse") == "http_response" + assert strings.snake_case("APIClientHTTPResponse") == "api_client_http_response" + assert strings.snake_case("OAuthClientHTTPResponse") == "o_auth_client_http_response" + assert strings.snake_case("S3Config") == "s3_config" + + +def test_snake_case_from_pascal_with_numbers(): + assert strings.snake_case("Response200") == "response_200" + assert strings.snake_case("Response200WithContent") == "response_200_with_content" + + +def test_snake_case_from_pascal(): + assert strings.snake_case("HttpResponsePascalCase") == "http_response_pascal_case" + + +def test_snake_case_from_camel(): + assert strings.snake_case("httpResponseLowerCamel") == "http_response_lower_camel" + assert strings.snake_case("connectionID") == "connection_id" + + +def test_snake_case_untrusted_string(): + assert strings.snake_case(UntrustedString("httpResponse")) == "http_response" + + +def test_kebab_case(): + assert strings.kebab_case("keep_alive") == "keep-alive" + + +def test_sanitize(): + assert strings.sanitize("some.thing*~with lots_- of weird things}=") == "some.thingwith lots_- of weird things" + + +def test_sanitize_strips_non_identifier_unicode(): + # ¼ matches \w but cannot appear in a Python identifier; digits must be kept for splitting + assert strings.sanitize("¼") == "" + assert strings.sanitize("S3Config") == "S3Config" + + +def test_split_words_on_backslash(): + # Backslash is a delimiter; it must actually split, not leak into the word + assert strings.split_words("one\\two") == ["one", "two"] + assert strings.snake_case("one\\two") == "one_two" + + +@pytest.mark.parametrize( + "value, expected", + [ + ('an "evil" string', r"an \"evil\" string"), + (r"an \"evil string", r"an \\\"evil string"), + ], +) +def test_no_string_escapes(value: str, expected: str): + assert strings.remove_string_escapes(value) == expected + + +@pytest.mark.parametrize( + "reserved_word, expected", + [ + ("self", "self_"), + ("int", "int_"), + ("dict", "dict_"), + ("not_reserved", "not_reserved"), + ("type", "type_"), + ("id", "id"), + ("None", "None_"), + ], +) +def test__fix_reserved_words(reserved_word: str, expected: str): + assert strings.fix_reserved_words(reserved_word) == expected + + +@pytest.mark.parametrize( + "before, after", + [ + ("PascalCase", "PascalCase"), + ("snake_case", "SnakeCase"), + ("TLAClass", "TLAClass"), + ("Title Case", "TitleCase"), + ("s3_config", "S3Config"), + ("__LeadingUnderscore", "LeadingUnderscore"), + ], +) +def test_pascalcase(before, after): + assert strings.pascal_case(before) == after + + +@pytest.mark.parametrize( + "content_type, expected", + [ + pytest.param("application/json", "application/json"), + pytest.param("application/vnd.api+json", "application/vnd.api+json"), + pytest.param("application/json;charset=utf-8", "application/json"), + pytest.param("application/vnd.api+json;charset=utf-8", "application/vnd.api+json"), + ], +) +def test_get_content_type(content_type: str, expected: str, config) -> None: + assert strings.get_content_type(content_type, config) == expected + + +@pytest.mark.parametrize( + "untrusted, expected", + [ + ("innocuous", "innocuous"), + ('can contain "quotes"', 'can contain "quotes"'), + ("backslash\\ should be escaped", "backslash\\\\ should be escaped"), + ('triple quotes """ are an issue', 'triple quotes \\"\\"\\" are an issue'), + # Control characters which are illegal in Python source must be stripped + ("null\x00byte", "nullbyte"), + ("bell\x07rings", "bellrings"), + ("newlines\nand\ttabs\r\nstay", "newlines\nand\ttabs\r\nstay"), + ], +) +def test_safe_for_docstring(untrusted: str, expected: str) -> None: + assert strings.safe_for_docstring(UntrustedString(untrusted)) == expected + + +def test_safe_for_docstring_non_string() -> None: + """Non-string values (e.g. dict examples) are stringified instead of crashing""" + assert strings.safe_for_docstring({"nested": 'has "quotes"'}) == "{'nested': 'has \"quotes\"'}" + + +@pytest.mark.parametrize( + "untrusted, expected", + [ + ("innocuous", "innocuous"), + ('quotes "break out" of literals', 'quotes \\"break out\\" of literals'), + ("backslash\\ escapes", "backslash\\\\ escapes"), + ("newlines\nand\ttabs", "newlinesand\\ttabs"), + ("backspace\bform-feed\f", "backspace\\bform-feed\\f"), + ("single 'quotes' are fine", "single 'quotes' are fine"), + ("astral character \U00010000", "astral character \U00010000"), + ("\x7f\N{NEXT LINE}\N{LINE SEPARATOR}\N{PARAGRAPH SEPARATOR}", ""), + ("bidirectional\u202etext", "bidirectionaltext"), + ], +) +def test_in_double_quote_literal(untrusted: str, expected: str) -> None: + assert strings.in_double_quote_literal(UntrustedString(untrusted)) == expected + + +@pytest.mark.parametrize( + "untrusted, expected", + [ + ("innocuous", "innocuous"), + ('quotes "break out"', 'quotes \\"break out\\"'), + ("{braces} inject code", "{{braces}} inject code"), + ("{x!r:>10}", "{{x!r:>10}}"), + ("backslash\\", "backslash\\\\"), + ], +) +def test_in_f_string_literal(untrusted: str, expected: str) -> None: + assert strings.in_f_string_literal(UntrustedString(untrusted)) == expected + + +class TestPythonCode: + def test_get_code_returns_raw_source(self) -> None: + assert strings.PythonCode("'some' + code").as_unembedded_code() == "'some' + code" + + def test_repr_does_not_contain_code(self) -> None: + """The repr is what a template that forgets to unwrap renders: it must never contain the code""" + code = strings.PythonCode('""" + print("uh oh")') + assert "print" not in repr(code) + assert "PythonCode object at 0x" in repr(code) + + def test_no_str(self) -> None: + """PythonCode must not be implicitly convertible to str, so str() falls back to the safe repr""" + assert "__str__" not in vars(strings.PythonCode) + code = strings.PythonCode("1 + 1") + assert str(code) == repr(code) + assert "1 + 1" not in str(code) + + +def test_in_double_quote_literal_accepts_python_code() -> None: + assert strings.in_double_quote_literal(strings.PythonCode('"quoted"')) == '\\"quoted\\"' + + +def test_safe_for_docstring_accepts_python_code() -> None: + assert strings.safe_for_docstring(strings.PythonCode('a"""b')) == 'a\\"\\"\\"b' + + +def _wordwrap_like_templates(text: str, width: int) -> str: + """Replicates the `wordwrap` filter as used in templates (per-paragraph textwrap)""" + return "\n".join( + "\n".join( + textwrap.wrap( + line, + width=width, + expand_tabs=False, + replace_whitespace=False, + break_long_words=True, + break_on_hyphens=True, + ) + ) + for line in text.splitlines() + ) + + +# Widths passed to the wordwrap filter in templates +@pytest.mark.parametrize("width", [90, 100, 101, 112, 116]) +def test_safe_for_docstring_output_survives_wordwrap(width: int) -> None: + """wordwrap hard-splits over-long words; escaped content must stay valid Python wherever it lands""" + nasty = ( + 'has """ triple \\ quotes and \U001060ef astral chars and \x00nulls ' * 5 + # One long unbroken word: \U and \N are hard SyntaxErrors if a split leaves them dangling + + r"C:\Users\New\Documents\foo" * 8 + + " " + + '"' * 200 + ) + escaped = strings.safe_for_docstring(nasty) + wrapped = _wordwrap_like_templates(escaped, width) + with warnings.catch_warnings(): + warnings.simplefilter("error") # invalid escape sequences would warn (SyntaxError in future Python) + parsed = ast.parse(f'"""\n{wrapped}\n"""') + docstring = parsed.body[0].value.value + assert isinstance(docstring, str) # nothing broke out of the docstring + + +def test_safe_for_docstring_chunks_long_words_at_escape_safe_points() -> None: + """Inserted line breaks must not interrupt an escape sequence or change the content""" + word = r"C:\Users\New\Documents\foo" * 8 + escaped = strings.safe_for_docstring(word) + lines = escaped.split("\n") + assert len(lines) > 1 + assert all(len(line) <= strings._MAX_ESCAPED_WORD_LENGTH for line in lines) + for line in lines: + trailing_backslashes = len(line) - len(line.rstrip("\\")) + assert trailing_backslashes % 2 == 0 + # Only newlines were inserted; the escaped content is otherwise untouched + assert escaped.replace("\n", "") == word.replace("\\", "\\\\") + + +def test_safe_for_docstring_does_not_chunk_words_without_backslashes() -> None: + """Words with no escape sequences (e.g. URLs) can be hard-split anywhere, so leave them alone""" + word = "https://example.com/" + "a" * 500 + assert strings.safe_for_docstring(word) == word diff --git a/tests/test_templates/conftest.py b/tests/test_templates/conftest.py index 09fd63f6c..e41b84308 100644 --- a/tests/test_templates/conftest.py +++ b/tests/test_templates/conftest.py @@ -4,9 +4,8 @@ @pytest.fixture(scope="session") def env() -> Environment: - from openapi_python_client import utils + from openapi_python_client import TEMPLATE_FILTERS - TEMPLATE_FILTERS = {"snakecase": utils.snake_case, "kebabcase": utils.kebab_case} env = Environment(loader=PackageLoader("openapi_python_client"), trim_blocks=True, lstrip_blocks=True) env.filters.update(TEMPLATE_FILTERS) return env diff --git a/tests/test_templates/test_endpoint_macros.py b/tests/test_templates/test_endpoint_macros.py new file mode 100644 index 000000000..c601e0e30 --- /dev/null +++ b/tests/test_templates/test_endpoint_macros.py @@ -0,0 +1,36 @@ +from types import SimpleNamespace + +from openapi_python_client.strings import PythonCode + + +def _render_docstring(env, return_string, is_detailed: bool) -> str: + template = env.get_template("endpoint_macros.py.jinja") + endpoint = SimpleNamespace(summary=None, description=None, list_all_parameters=lambda: []) + return template.module.docstring(endpoint, return_string, is_detailed) + + +def test_endpoint_docstring_escapes_return_string(env) -> None: + """The return type is rendered inside endpoint docstrings; it must not be able to break out. + + Type strings can embed literal values (e.g. const `Literal[...]`), so the docstring macro must pass the + return string through `safe_for_docstring` rather than rendering the raw code. Note that a raw breakout + would still be *valid* Python (that's the point of an escape), so this asserts on the docstring content, + not just parseability. + """ + nasty = 'Literal[\'""" + print("uh oh") + """\']' + + for is_detailed in (True, False): + result = _render_docstring(env, nasty, is_detailed) + # Only the two docstring delimiters; the value's triple-quotes must be escaped + assert result.count('"""') == 2 + assert '\\"\\"\\"' in result + + +def test_endpoint_docstring_unwraps_python_code_return_string(env) -> None: + """A PythonCode return type must render its (escaped) code in the docstring, not the safe pointer repr""" + code = PythonCode("MyModel | None") + + result = _render_docstring(env, code, is_detailed=False) + + assert "MyModel | None" in result + assert "object at 0x" not in result diff --git a/tests/test_templates/test_model_template.py b/tests/test_templates/test_model_template.py new file mode 100644 index 000000000..1443f6c98 --- /dev/null +++ b/tests/test_templates/test_model_template.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from types import SimpleNamespace + +from openapi_python_client import UntrustedString + + +def _section(content: str, start: str, end: str | None = None) -> str: + section = content.split(start, 1)[1] + if end is not None: + section = section.split(end, 1)[0] + return section + + +def test_model_template_renders_lazy_imports_in_stable_order(env) -> None: + template = env.get_template("model.py.jinja") + + model = SimpleNamespace( + is_multipart_body=True, + relative_imports=set(), + lazy_imports={"from ..models.z import Z", "from ..models.a import A"}, + additional_properties=False, + class_info=SimpleNamespace(name="MyModel", module_name="my_model"), + title=UntrustedString(""), + description=UntrustedString(""), + example=UntrustedString(""), + required_properties=[], + optional_properties=[], + ) + config = SimpleNamespace(docstrings_on_attributes=False) + + content = template.render(model=model, config=config) + + sections = [ + _section(content, "if TYPE_CHECKING:", "T = TypeVar"), + _section(content, "def to_dict(self)", "def to_multipart(self)"), + _section(content, "def to_multipart(self)", "@classmethod"), + _section(content, "def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:", "return my_model"), + ] + for section in sections: + assert section.index("from ..models.a import A") < section.index("from ..models.z import Z") diff --git a/tests/test_templates/test_property_templates/test_date_property/date_property_template.py.jinja b/tests/test_templates/test_property_templates/test_date_property/date_property_template.py.jinja index 3709963fe..071aef509 100644 --- a/tests/test_templates/test_property_templates/test_date_property/date_property_template.py.jinja +++ b/tests/test_templates/test_property_templates/test_date_property/date_property_template.py.jinja @@ -1,7 +1,6 @@ +import datetime from datetime import date -from typing import cast, Union -from dateutil.parser import isoparse {% from "property_templates/date_property.py.jinja" import transform, construct %} some_source = date(2020, 10, 12) {{ transform(property, "some_source", "some_destination") }} diff --git a/tests/test_templates/test_property_templates/test_date_property/required_not_null.py b/tests/test_templates/test_property_templates/test_date_property/required_not_null.py index ad4f380a4..f27bc455f 100644 --- a/tests/test_templates/test_property_templates/test_date_property/required_not_null.py +++ b/tests/test_templates/test_property_templates/test_date_property/required_not_null.py @@ -1,9 +1,8 @@ +import datetime from datetime import date -from typing import cast, Union -from dateutil.parser import isoparse some_source = date(2020, 10, 12) some_destination = some_source.isoformat() -a_prop = isoparse(some_destination).date() +a_prop = datetime.date.fromisoformat(some_destination) diff --git a/tests/test_templates/test_property_templates/test_datetime_property/datetime_property_template.py.jinja b/tests/test_templates/test_property_templates/test_datetime_property/datetime_property_template.py.jinja index 85fa1548d..3b428734d 100644 --- a/tests/test_templates/test_property_templates/test_datetime_property/datetime_property_template.py.jinja +++ b/tests/test_templates/test_property_templates/test_datetime_property/datetime_property_template.py.jinja @@ -1,7 +1,6 @@ +import datetime from datetime import date -from typing import cast, Union -from dateutil.parser import isoparse {% from "property_templates/datetime_property.py.jinja" import transform, construct %} some_source = date(2020, 10, 12) {{ transform(property, "some_source", "some_destination") }} diff --git a/tests/test_templates/test_property_templates/test_datetime_property/required_not_null.py b/tests/test_templates/test_property_templates/test_datetime_property/required_not_null.py index 8253828e3..c55e883d7 100644 --- a/tests/test_templates/test_property_templates/test_datetime_property/required_not_null.py +++ b/tests/test_templates/test_property_templates/test_datetime_property/required_not_null.py @@ -1,9 +1,8 @@ +import datetime from datetime import date -from typing import cast, Union -from dateutil.parser import isoparse some_source = date(2020, 10, 12) some_destination = some_source.isoformat() -a_prop = isoparse(some_destination) +a_prop = datetime.datetime.fromisoformat(some_destination) diff --git a/tests/test_templates/test_property_templates/test_uuid_property/__init__.py b/tests/test_templates/test_property_templates/test_uuid_property/__init__.py new file mode 100644 index 000000000..839bd41fd --- /dev/null +++ b/tests/test_templates/test_property_templates/test_uuid_property/__init__.py @@ -0,0 +1 @@ +"""Tests for UUID property templates.""" diff --git a/tests/test_templates/test_property_templates/test_uuid_property/test_uuid_multipart.py b/tests/test_templates/test_property_templates/test_uuid_property/test_uuid_multipart.py new file mode 100644 index 000000000..224b7b855 --- /dev/null +++ b/tests/test_templates/test_property_templates/test_uuid_property/test_uuid_multipart.py @@ -0,0 +1,70 @@ +"""Tests for UUID property multipart macro functionality.""" + +from pathlib import Path +from typing import Any +from uuid import UUID + +import jinja2 +import pytest + +from openapi_python_client.parser.properties import UuidProperty +from openapi_python_client.strings import PythonIdentifier + + +def uuid_property(required: bool = True, default: Any = None) -> UuidProperty: + """Helper to create a UuidProperty for testing.""" + return UuidProperty( + name="test_uuid", + required=required, + default=default, + python_name=PythonIdentifier(value="test_uuid", prefix=""), + description="A test UUID property", + example="550e8400-e29b-41d4-a716-446655440000", + ) + + +@pytest.fixture +def jinja_env() -> jinja2.Environment: + """Create a Jinja2 environment with the property templates loaded.""" + templates_dir = Path(__file__).parent.parent.parent.parent.parent / "openapi_python_client" / "templates" + env = jinja2.Environment( + loader=jinja2.FileSystemLoader(templates_dir), + trim_blocks=True, + lstrip_blocks=True, + ) + return env + + +def test_multipart_macro_generates_syntactically_correct_code_for_required_uuid(jinja_env: jinja2.Environment) -> None: + """Test that the multipart macro generates syntactically correct Python code for required UUID properties.""" + prop = uuid_property(required=True) + + template = jinja_env.get_template("property_templates/uuid_property.py.jinja") + + # Render the multipart macro + multipart_code = template.module.multipart(prop, "test_uuid", '"test_uuid"') # type: ignore[attr-defined] + + # Verify the generated code is syntactically correct + expected = 'files.append(("test_uuid", (None, str(test_uuid), "text/plain")))' + assert multipart_code.strip() == expected + + # Verify it compiles as valid Python + compile(multipart_code, "", "exec") + + +def test_multipart_macro_generates_syntactically_correct_code_for_optional_uuid(jinja_env: jinja2.Environment) -> None: + """Test that the multipart macro generates syntactically correct Python code for optional UUID properties.""" + prop = uuid_property(required=False) + + template = jinja_env.get_template("property_templates/uuid_property.py.jinja") + + # Render the multipart macro + multipart_code = template.module.multipart(prop, "test_uuid", '"test_uuid"') # type: ignore[attr-defined] + + # Verify the generated code is syntactically correct + expected = 'files.append(("test_uuid", (None, str(test_uuid), "text/plain")))' + assert multipart_code.strip() == expected + + # Verify it compiles as valid Python + compile(multipart_code, "", "exec") + \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index fafa61805..000000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,136 +0,0 @@ -import pytest - -from openapi_python_client import utils - - -class TestPythonIdentifier: - def test_valid_identifier_is_not_changed(self): - assert utils.PythonIdentifier(value="valid_field", prefix="field") == "valid_field" - - def test_numbers_are_prefixed(self): - assert utils.PythonIdentifier(value="1", prefix="field") == "field1" - - def test_invalid_symbols_are_stripped(self): - assert utils.PythonIdentifier(value="$abc", prefix="prefix") == "abc" - - def test_keywords_are_postfixed(self): - assert utils.PythonIdentifier(value="for", prefix="prefix") == "for_" - - def test_empty_is_prefixed(self): - assert utils.PythonIdentifier(value="", prefix="something") == "something" - - -class TestClassName: - def test_valid_is_not_changed(self): - assert utils.ClassName(value="ValidClass", prefix="field") == "ValidClass" - - def test_numbers_are_prefixed(self): - assert utils.ClassName(value="1", prefix="field") == "Field1" - - def test_invalid_symbols_are_stripped(self): - assert utils.ClassName(value="$abc", prefix="prefix") == "Abc" - - def test_keywords_are_postfixed(self): - assert utils.ClassName(value="none", prefix="prefix") == "None_" - - def test_empty_is_prefixed(self): - assert utils.ClassName(value="", prefix="something") == "Something" - - -@pytest.mark.parametrize( - "before, after", - [ - ("connectionID", ["connection", "ID"]), - ("connection_id", ["connection", "id"]), - ("connection-id", ["connection", "id"]), - ("Response200", ["Response", "200"]), - ("Response200Okay", ["Response", "200", "Okay"]), - ("S3Config", ["S3", "Config"]), - ("s3config", ["s3config"]), - ("fully.qualified.Name", ["fully", "qualified", "Name"]), - ], -) -def test_split_words(before, after): - assert utils.split_words(before) == after - - -def test_snake_case_uppercase_str(): - assert utils.snake_case("HTTP") == "http" - assert utils.snake_case("HTTP RESPONSE") == "http_response" - - -def test_snake_case_from_pascal_with_acronyms(): - assert utils.snake_case("HTTPResponse") == "http_response" - assert utils.snake_case("APIClientHTTPResponse") == "api_client_http_response" - assert utils.snake_case("OAuthClientHTTPResponse") == "o_auth_client_http_response" - assert utils.snake_case("S3Config") == "s3_config" - - -def test_snake_case_from_pascal_with_numbers(): - assert utils.snake_case("Response200") == "response_200" - assert utils.snake_case("Response200WithContent") == "response_200_with_content" - - -def test_snake_case_from_pascal(): - assert utils.snake_case("HttpResponsePascalCase") == "http_response_pascal_case" - - -def test_snake_case_from_camel(): - assert utils.snake_case("httpResponseLowerCamel") == "http_response_lower_camel" - assert utils.snake_case("connectionID") == "connection_id" - - -def test_kebab_case(): - assert utils.kebab_case("keep_alive") == "keep-alive" - - -def test_sanitize(): - assert utils.sanitize("some.thing*~with lots_- of weird things}=") == "some.thingwith lots_- of weird things" - - -def test_no_string_escapes(): - assert utils.remove_string_escapes('an "evil" string') == 'an \\"evil\\" string' - - -@pytest.mark.parametrize( - "reserved_word, expected", - [ - ("self", "self_"), - ("int", "int_"), - ("dict", "dict_"), - ("not_reserved", "not_reserved"), - ("type", "type_"), - ("id", "id"), - ("None", "None_"), - ], -) -def test__fix_reserved_words(reserved_word: str, expected: str): - assert utils.fix_reserved_words(reserved_word) == expected - - -@pytest.mark.parametrize( - "before, after", - [ - ("PascalCase", "PascalCase"), - ("snake_case", "SnakeCase"), - ("TLAClass", "TLAClass"), - ("Title Case", "TitleCase"), - ("s3_config", "S3Config"), - ("__LeadingUnderscore", "LeadingUnderscore"), - ], -) -def test_pascalcase(before, after): - assert utils.pascal_case(before) == after - - -@pytest.mark.parametrize( - "content_type, expected", - [ - pytest.param("application/json", "application/json"), - pytest.param("application/vnd.api+json", "application/vnd.api+json"), - pytest.param("application/json;charset=utf-8", "application/json"), - pytest.param("application/vnd.api+json;charset=utf-8", "application/vnd.api+json"), - ], -) -def test_get_content_type(content_type: str, expected: str, config) -> None: - assert utils.get_content_type(content_type, config) == expected