From c69bd8f354fc5b45d263dda460efd91d8fe5f4ce Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 06:33:07 +0000 Subject: [PATCH 001/116] chore: configure new SDK language --- .devcontainer/Dockerfile | 9 + .devcontainer/devcontainer.json | 43 + .github/workflows/ci.yml | 96 + .gitignore | 15 + .python-version | 1 + .stats.yml | 4 + .vscode/settings.json | 3 + Brewfile | 2 + CONTRIBUTING.md | 128 ++ LICENSE | 201 ++ README.md | 371 ++- SECURITY.md | 27 + api.md | 26 + bin/publish-pypi | 6 + examples/.keep | 4 + mypy.ini | 50 + noxfile.py | 9 + pyproject.toml | 211 ++ requirements-dev.lock | 135 ++ requirements.lock | 72 + scripts/bootstrap | 19 + scripts/format | 8 + scripts/lint | 11 + scripts/mock | 41 + scripts/test | 61 + scripts/utils/ruffen-docs.py | 167 ++ scripts/utils/upload-artifact.sh | 27 + src/cas_parser/__init__.py | 102 + src/cas_parser/_base_client.py | 1995 +++++++++++++++++ src/cas_parser/_client.py | 469 ++++ src/cas_parser/_compat.py | 219 ++ src/cas_parser/_constants.py | 14 + src/cas_parser/_exceptions.py | 108 + src/cas_parser/_files.py | 123 + src/cas_parser/_models.py | 829 +++++++ src/cas_parser/_qs.py | 150 ++ src/cas_parser/_resource.py | 43 + src/cas_parser/_response.py | 832 +++++++ src/cas_parser/_streaming.py | 333 +++ src/cas_parser/_types.py | 219 ++ src/cas_parser/_utils/__init__.py | 57 + src/cas_parser/_utils/_logs.py | 25 + src/cas_parser/_utils/_proxy.py | 65 + src/cas_parser/_utils/_reflection.py | 42 + src/cas_parser/_utils/_resources_proxy.py | 24 + src/cas_parser/_utils/_streams.py | 12 + src/cas_parser/_utils/_sync.py | 86 + src/cas_parser/_utils/_transform.py | 447 ++++ src/cas_parser/_utils/_typing.py | 151 ++ src/cas_parser/_utils/_utils.py | 422 ++++ src/cas_parser/_version.py | 4 + src/cas_parser/lib/.keep | 4 + src/cas_parser/py.typed | 0 src/cas_parser/resources/__init__.py | 33 + src/cas_parser/resources/cas_generator.py | 225 ++ src/cas_parser/resources/cas_parser.py | 592 +++++ src/cas_parser/types/__init__.py | 11 + .../cas_generator_generate_cas_params.py | 30 + .../cas_generator_generate_cas_response.py | 13 + .../types/cas_parser_cams_kfintech_params.py | 18 + .../types/cas_parser_cdsl_params.py | 18 + .../types/cas_parser_nsdl_params.py | 18 + .../types/cas_parser_smart_parse_params.py | 18 + src/cas_parser/types/unified_response.py | 432 ++++ tests/__init__.py | 1 + tests/api_resources/__init__.py | 1 + tests/api_resources/test_cas_generator.py | 136 ++ tests/api_resources/test_cas_parser.py | 330 +++ tests/conftest.py | 84 + tests/sample_file.txt | 1 + tests/test_client.py | 1748 +++++++++++++++ tests/test_deepcopy.py | 58 + tests/test_extract_files.py | 64 + tests/test_files.py | 51 + tests/test_models.py | 963 ++++++++ tests/test_qs.py | 78 + tests/test_required_args.py | 111 + tests/test_response.py | 277 +++ tests/test_streaming.py | 248 ++ tests/test_transform.py | 453 ++++ tests/test_utils/test_proxy.py | 34 + tests/test_utils/test_typing.py | 73 + tests/utils.py | 159 ++ 83 files changed, 14799 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 .stats.yml create mode 100644 .vscode/settings.json create mode 100644 Brewfile create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 api.md create mode 100644 bin/publish-pypi create mode 100644 examples/.keep create mode 100644 mypy.ini create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 requirements-dev.lock create mode 100644 requirements.lock create mode 100755 scripts/bootstrap create mode 100755 scripts/format create mode 100755 scripts/lint create mode 100755 scripts/mock create mode 100755 scripts/test create mode 100644 scripts/utils/ruffen-docs.py create mode 100755 scripts/utils/upload-artifact.sh create mode 100644 src/cas_parser/__init__.py create mode 100644 src/cas_parser/_base_client.py create mode 100644 src/cas_parser/_client.py create mode 100644 src/cas_parser/_compat.py create mode 100644 src/cas_parser/_constants.py create mode 100644 src/cas_parser/_exceptions.py create mode 100644 src/cas_parser/_files.py create mode 100644 src/cas_parser/_models.py create mode 100644 src/cas_parser/_qs.py create mode 100644 src/cas_parser/_resource.py create mode 100644 src/cas_parser/_response.py create mode 100644 src/cas_parser/_streaming.py create mode 100644 src/cas_parser/_types.py create mode 100644 src/cas_parser/_utils/__init__.py create mode 100644 src/cas_parser/_utils/_logs.py create mode 100644 src/cas_parser/_utils/_proxy.py create mode 100644 src/cas_parser/_utils/_reflection.py create mode 100644 src/cas_parser/_utils/_resources_proxy.py create mode 100644 src/cas_parser/_utils/_streams.py create mode 100644 src/cas_parser/_utils/_sync.py create mode 100644 src/cas_parser/_utils/_transform.py create mode 100644 src/cas_parser/_utils/_typing.py create mode 100644 src/cas_parser/_utils/_utils.py create mode 100644 src/cas_parser/_version.py create mode 100644 src/cas_parser/lib/.keep create mode 100644 src/cas_parser/py.typed create mode 100644 src/cas_parser/resources/__init__.py create mode 100644 src/cas_parser/resources/cas_generator.py create mode 100644 src/cas_parser/resources/cas_parser.py create mode 100644 src/cas_parser/types/__init__.py create mode 100644 src/cas_parser/types/cas_generator_generate_cas_params.py create mode 100644 src/cas_parser/types/cas_generator_generate_cas_response.py create mode 100644 src/cas_parser/types/cas_parser_cams_kfintech_params.py create mode 100644 src/cas_parser/types/cas_parser_cdsl_params.py create mode 100644 src/cas_parser/types/cas_parser_nsdl_params.py create mode 100644 src/cas_parser/types/cas_parser_smart_parse_params.py create mode 100644 src/cas_parser/types/unified_response.py create mode 100644 tests/__init__.py create mode 100644 tests/api_resources/__init__.py create mode 100644 tests/api_resources/test_cas_generator.py create mode 100644 tests/api_resources/test_cas_parser.py create mode 100644 tests/conftest.py create mode 100644 tests/sample_file.txt create mode 100644 tests/test_client.py create mode 100644 tests/test_deepcopy.py create mode 100644 tests/test_extract_files.py create mode 100644 tests/test_files.py create mode 100644 tests/test_models.py create mode 100644 tests/test_qs.py create mode 100644 tests/test_required_args.py create mode 100644 tests/test_response.py create mode 100644 tests/test_streaming.py create mode 100644 tests/test_transform.py create mode 100644 tests/test_utils/test_proxy.py create mode 100644 tests/test_utils/test_typing.py create mode 100644 tests/utils.py diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..ff261ba --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +ARG VARIANT="3.9" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +USER vscode + +RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash +ENV PATH=/home/vscode/.rye/shims:$PATH + +RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c17fdc1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,43 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Debian", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + + "postStartCommand": "rye sync --all-features", + + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python" + ], + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": ".venv/bin/python", + "python.defaultInterpreterPath": ".venv/bin/python", + "python.typeChecking": "basic", + "terminal.integrated.env.linux": { + "PATH": "/home/vscode/.rye/shims:${env:PATH}" + } + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": {} + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7d5023d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,96 @@ +name: CI +on: + push: + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'stl-preview-head/**' + - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' + +jobs: + lint: + timeout-minutes: 10 + name: lint + runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: rye sync --all-features + + - name: Run lints + run: ./scripts/lint + + build: + if: github.repository == 'stainless-sdks/cas-parser-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + timeout-minutes: 10 + name: build + permissions: + contents: read + id-token: write + runs-on: depot-ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: rye sync --all-features + + - name: Run build + run: rye build + + - name: Get GitHub OIDC Token + id: github-oidc + uses: actions/github-script@v6 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Upload tarball + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + run: ./scripts/utils/upload-artifact.sh + + test: + timeout-minutes: 10 + name: test + runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run tests + run: ./scripts/test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95ceb18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.prism.log +_dev + +__pycache__ +.mypy_cache + +dist + +.venv +.idea + +.env +.envrc +codegen.log +Brewfile.lock.json diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..43077b2 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.18 diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 0000000..36105c2 --- /dev/null +++ b/.stats.yml @@ -0,0 +1,4 @@ +configured_endpoints: 5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml +openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff +config_hash: e5b8a95b93a04cfe1a8b6546333954ac diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5b01030 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.importFormat": "relative", +} diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..492ca37 --- /dev/null +++ b/Brewfile @@ -0,0 +1,2 @@ +brew "rye" + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c565215 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,128 @@ +## Setting up the environment + +### With Rye + +We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: + +```sh +$ ./scripts/bootstrap +``` + +Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: + +```sh +$ rye sync --all-features +``` + +You can then run scripts using `rye run python script.py` or by activating the virtual environment: + +```sh +# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work +$ source .venv/bin/activate + +# now you can omit the `rye run` prefix +$ python script.py +``` + +### Without Rye + +Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: + +```sh +$ pip install -r requirements-dev.lock +``` + +## Modifying/Adding code + +Most of the SDK is generated code. Modifications to code will be persisted between generations, but may +result in merge conflicts between manual patches and changes from the generator. The generator will never +modify the contents of the `src/cas_parser/lib/` and `examples/` directories. + +## Adding and running examples + +All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. + +```py +# add an example to examples/.py + +#!/usr/bin/env -S rye run python +… +``` + +```sh +$ chmod +x examples/.py +# run the example against your api +$ ./examples/.py +``` + +## Using the repository from source + +If you’d like to use the repository from source, you can either install from git or link to a cloned repository: + +To install via git: + +```sh +$ pip install git+ssh://git@github.com/stainless-sdks/cas-parser-python.git +``` + +Alternatively, you can build from source and install the wheel file: + +Building this package will create two files in the `dist/` directory, a `.tar.gz` containing the source files and a `.whl` that can be used to install the package efficiently. + +To create a distributable version of the library, all you have to do is run this command: + +```sh +$ rye build +# or +$ python -m build +``` + +Then to install: + +```sh +$ pip install ./path-to-wheel-file.whl +``` + +## Running tests + +Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. + +```sh +# you will need npm installed +$ npx prism mock path/to/your/openapi.yml +``` + +```sh +$ ./scripts/test +``` + +## Linting and formatting + +This repository uses [ruff](https://github.com/astral-sh/ruff) and +[black](https://github.com/psf/black) to format the code in the repository. + +To lint: + +```sh +$ ./scripts/lint +``` + +To format and fix all ruff issues automatically: + +```sh +$ ./scripts/format +``` + +## Publishing and releases + +Changes made to this repository via the automated release PR pipeline should publish to PyPI automatically. If +the changes aren't made through the automated pipeline, you may want to make releases manually. + +### Publish with a GitHub workflow + +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/cas-parser-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. + +### Publish manually + +If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on +the environment. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f1756ce --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Cas Parser + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 8d45e75..0843fd6 100644 --- a/README.md +++ b/README.md @@ -1 +1,370 @@ -# cas-parser-python \ No newline at end of file +# Cas Parser Python API library + + +[![PyPI version](https://img.shields.io/pypi/v/cas_parser.svg?label=pypi%20(stable))](https://pypi.org/project/cas_parser/) + +The Cas Parser Python library provides convenient access to the Cas Parser REST API from any Python 3.8+ +application. The library includes type definitions for all request params and response fields, +and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). + +It is generated with [Stainless](https://www.stainless.com/). + +## Documentation + +The REST API documentation can be found on [docs.casparser.in](https://docs.casparser.in/reference). The full API of this library can be found in [api.md](api.md). + +## Installation + +```sh +# install from this staging repo +pip install git+ssh://git@github.com/stainless-sdks/cas-parser-python.git +``` + +> [!NOTE] +> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install cas_parser` + +## Usage + +The full API of this library can be found in [api.md](api.md). + +```python +import os +from cas_parser import CasParser + +client = CasParser( + api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted + # defaults to "production". + environment="local", +) + +unified_response = client.cas_parser.cams_kfintech() +print(unified_response.demat_accounts) +``` + +While you can provide an `api_key` keyword argument, +we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) +to add `CAS_PARSER_API_KEY="My API Key"` to your `.env` file +so that your API Key is not stored in source control. + +## Async usage + +Simply import `AsyncCasParser` instead of `CasParser` and use `await` with each API call: + +```python +import os +import asyncio +from cas_parser import AsyncCasParser + +client = AsyncCasParser( + api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted + # defaults to "production". + environment="local", +) + + +async def main() -> None: + unified_response = await client.cas_parser.cams_kfintech() + print(unified_response.demat_accounts) + + +asyncio.run(main()) +``` + +Functionality between the synchronous and asynchronous clients is otherwise identical. + +### With aiohttp + +By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. + +You can enable this by installing `aiohttp`: + +```sh +# install from this staging repo +pip install 'cas_parser[aiohttp] @ git+ssh://git@github.com/stainless-sdks/cas-parser-python.git' +``` + +Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: + +```python +import asyncio +from cas_parser import DefaultAioHttpClient +from cas_parser import AsyncCasParser + + +async def main() -> None: + async with AsyncCasParser( + api_key="My API Key", + http_client=DefaultAioHttpClient(), + ) as client: + unified_response = await client.cas_parser.cams_kfintech() + print(unified_response.demat_accounts) + + +asyncio.run(main()) +``` + +## Using types + +Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: + +- Serializing back into JSON, `model.to_json()` +- Converting to a dictionary, `model.to_dict()` + +Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. + +## Handling errors + +When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `cas_parser.APIConnectionError` is raised. + +When the API returns a non-success status code (that is, 4xx or 5xx +response), a subclass of `cas_parser.APIStatusError` is raised, containing `status_code` and `response` properties. + +All errors inherit from `cas_parser.APIError`. + +```python +import cas_parser +from cas_parser import CasParser + +client = CasParser() + +try: + client.cas_parser.cams_kfintech() +except cas_parser.APIConnectionError as e: + print("The server could not be reached") + print(e.__cause__) # an underlying Exception, likely raised within httpx. +except cas_parser.RateLimitError as e: + print("A 429 status code was received; we should back off a bit.") +except cas_parser.APIStatusError as e: + print("Another non-200-range status code was received") + print(e.status_code) + print(e.response) +``` + +Error codes are as follows: + +| Status Code | Error Type | +| ----------- | -------------------------- | +| 400 | `BadRequestError` | +| 401 | `AuthenticationError` | +| 403 | `PermissionDeniedError` | +| 404 | `NotFoundError` | +| 422 | `UnprocessableEntityError` | +| 429 | `RateLimitError` | +| >=500 | `InternalServerError` | +| N/A | `APIConnectionError` | + +### Retries + +Certain errors are automatically retried 2 times by default, with a short exponential backoff. +Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, +429 Rate Limit, and >=500 Internal errors are all retried by default. + +You can use the `max_retries` option to configure or disable retry settings: + +```python +from cas_parser import CasParser + +# Configure the default for all requests: +client = CasParser( + # default is 2 + max_retries=0, +) + +# Or, configure per-request: +client.with_options(max_retries=5).cas_parser.cams_kfintech() +``` + +### Timeouts + +By default requests time out after 1 minute. You can configure this with a `timeout` option, +which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: + +```python +from cas_parser import CasParser + +# Configure the default for all requests: +client = CasParser( + # 20 seconds (default is 1 minute) + timeout=20.0, +) + +# More granular control: +client = CasParser( + timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), +) + +# Override per-request: +client.with_options(timeout=5.0).cas_parser.cams_kfintech() +``` + +On timeout, an `APITimeoutError` is thrown. + +Note that requests that time out are [retried twice by default](#retries). + +## Advanced + +### Logging + +We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. + +You can enable logging by setting the environment variable `CAS_PARSER_LOG` to `info`. + +```shell +$ export CAS_PARSER_LOG=info +``` + +Or to `debug` for more verbose logging. + +### How to tell whether `None` means `null` or missing + +In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: + +```py +if response.my_field is None: + if 'my_field' not in response.model_fields_set: + print('Got json like {}, without a "my_field" key present at all.') + else: + print('Got json like {"my_field": null}.') +``` + +### Accessing raw response data (e.g. headers) + +The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., + +```py +from cas_parser import CasParser + +client = CasParser() +response = client.cas_parser.with_raw_response.cams_kfintech() +print(response.headers.get('X-My-Header')) + +cas_parser = response.parse() # get the object that `cas_parser.cams_kfintech()` would have returned +print(cas_parser.demat_accounts) +``` + +These methods return an [`APIResponse`](https://github.com/stainless-sdks/cas-parser-python/tree/main/src/cas_parser/_response.py) object. + +The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/cas-parser-python/tree/main/src/cas_parser/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. + +#### `.with_streaming_response` + +The above interface eagerly reads the full response body when you make the request, which may not always be what you want. + +To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. + +```python +with client.cas_parser.with_streaming_response.cams_kfintech() as response: + print(response.headers.get("X-My-Header")) + + for line in response.iter_lines(): + print(line) +``` + +The context manager is required so that the response will reliably be closed. + +### Making custom/undocumented requests + +This library is typed for convenient access to the documented API. + +If you need to access undocumented endpoints, params, or response properties, the library can still be used. + +#### Undocumented endpoints + +To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other +http verbs. Options on the client will be respected (such as retries) when making this request. + +```py +import httpx + +response = client.post( + "/foo", + cast_to=httpx.Response, + body={"my_param": True}, +) + +print(response.headers.get("x-foo")) +``` + +#### Undocumented request params + +If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request +options. + +#### Undocumented response properties + +To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You +can also get all the extra fields on the Pydantic model as a dict with +[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). + +### Configuring the HTTP client + +You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: + +- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) +- Custom [transports](https://www.python-httpx.org/advanced/transports/) +- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality + +```python +import httpx +from cas_parser import CasParser, DefaultHttpxClient + +client = CasParser( + # Or use the `CAS_PARSER_BASE_URL` env var + base_url="http://my.test.server.example.com:8083", + http_client=DefaultHttpxClient( + proxy="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + +You can also customize the client on a per-request basis by using `with_options()`: + +```python +client.with_options(http_client=DefaultHttpxClient(...)) +``` + +### Managing HTTP resources + +By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. + +```py +from cas_parser import CasParser + +with CasParser() as client: + # make requests here + ... + +# HTTP client is now closed +``` + +## Versioning + +This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: + +1. Changes that only affect static types, without breaking runtime behavior. +2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ +3. Changes that we do not expect to impact the vast majority of users in practice. + +We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. + +We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/cas-parser-python/issues) with questions, bugs, or suggestions. + +### Determining the installed version + +If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. + +You can determine the version that is being used at runtime with: + +```py +import cas_parser +print(cas_parser.__version__) +``` + +## Requirements + +Python 3.8 or higher. + +## Contributing + +See [the contributing documentation](./CONTRIBUTING.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..62f7dbe --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security Policy + +## Reporting Security Issues + +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. + +To report a security issue, please contact the Stainless team at security@stainless.com. + +## Responsible Disclosure + +We appreciate the efforts of security researchers and individuals who help us maintain the security of +SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible +disclosure practices by allowing us a reasonable amount of time to investigate and address the issue +before making any information public. + +## Reporting Non-SDK Related Security Issues + +If you encounter security issues that are not directly related to SDKs but pertain to the services +or products provided by Cas Parser, please follow the respective company's security reporting guidelines. + +### Cas Parser Terms and Policies + +Please contact sameer@casparser.in for any questions or concerns regarding the security of our services. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/api.md b/api.md new file mode 100644 index 0000000..9f56f41 --- /dev/null +++ b/api.md @@ -0,0 +1,26 @@ +# CasParser + +Types: + +```python +from cas_parser.types import UnifiedResponse +``` + +Methods: + +- client.cas_parser.cams_kfintech(\*\*params) -> UnifiedResponse +- client.cas_parser.cdsl(\*\*params) -> UnifiedResponse +- client.cas_parser.nsdl(\*\*params) -> UnifiedResponse +- client.cas_parser.smart_parse(\*\*params) -> UnifiedResponse + +# CasGenerator + +Types: + +```python +from cas_parser.types import CasGeneratorGenerateCasResponse +``` + +Methods: + +- client.cas_generator.generate_cas(\*\*params) -> CasGeneratorGenerateCasResponse diff --git a/bin/publish-pypi b/bin/publish-pypi new file mode 100644 index 0000000..826054e --- /dev/null +++ b/bin/publish-pypi @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -eux +mkdir -p dist +rye build --clean +rye publish --yes --token=$PYPI_TOKEN diff --git a/examples/.keep b/examples/.keep new file mode 100644 index 0000000..d8c73e9 --- /dev/null +++ b/examples/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store example files demonstrating usage of this SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..14b5020 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,50 @@ +[mypy] +pretty = True +show_error_codes = True + +# Exclude _files.py because mypy isn't smart enough to apply +# the correct type narrowing and as this is an internal module +# it's fine to just use Pyright. +# +# We also exclude our `tests` as mypy doesn't always infer +# types correctly and Pyright will still catch any type errors. +exclude = ^(src/cas_parser/_files\.py|_dev/.*\.py|tests/.*)$ + +strict_equality = True +implicit_reexport = True +check_untyped_defs = True +no_implicit_optional = True + +warn_return_any = True +warn_unreachable = True +warn_unused_configs = True + +# Turn these options off as it could cause conflicts +# with the Pyright options. +warn_unused_ignores = False +warn_redundant_casts = False + +disallow_any_generics = True +disallow_untyped_defs = True +disallow_untyped_calls = True +disallow_subclassing_any = True +disallow_incomplete_defs = True +disallow_untyped_decorators = True +cache_fine_grained = True + +# By default, mypy reports an error if you assign a value to the result +# of a function call that doesn't return anything. We do this in our test +# cases: +# ``` +# result = ... +# assert result is None +# ``` +# Changing this codegen to make mypy happy would increase complexity +# and would not be worth it. +disable_error_code = func-returns-value,overload-cannot-match + +# https://github.com/python/mypy/issues/12162 +[mypy.overrides] +module = "black.files.*" +ignore_errors = true +ignore_missing_imports = true diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..53bca7f --- /dev/null +++ b/noxfile.py @@ -0,0 +1,9 @@ +import nox + + +@nox.session(reuse_venv=True, name="test-pydantic-v1") +def test_pydantic_v1(session: nox.Session) -> None: + session.install("-r", "requirements-dev.lock") + session.install("pydantic<2") + + session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2c2af13 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,211 @@ +[project] +name = "cas_parser" +version = "0.0.1" +description = "The official Python library for the CAS Parser API" +dynamic = ["readme"] +license = "Apache-2.0" +authors = [ +{ name = "Cas Parser", email = "sameer@casparser.in" }, +] +dependencies = [ + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", +] +requires-python = ">= 3.8" +classifiers = [ + "Typing :: Typed", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3.8", + "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", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: Apache Software License" +] + +[project.urls] +Homepage = "https://github.com/stainless-sdks/cas-parser-python" +Repository = "https://github.com/stainless-sdks/cas-parser-python" + +[project.optional-dependencies] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] + +[tool.rye] +managed = true +# version pins are in requirements-dev.lock +dev-dependencies = [ + "pyright==1.1.399", + "mypy", + "respx", + "pytest", + "pytest-asyncio", + "ruff", + "time-machine", + "nox", + "dirty-equals>=0.6.0", + "importlib-metadata>=6.7.0", + "rich>=13.7.1", + "nest_asyncio==1.6.0", + "pytest-xdist>=3.6.1", +] + +[tool.rye.scripts] +format = { chain = [ + "format:ruff", + "format:docs", + "fix:ruff", + # run formatting again to fix any inconsistencies when imports are stripped + "format:ruff", +]} +"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" +"format:ruff" = "ruff format" + +"lint" = { chain = [ + "check:ruff", + "typecheck", + "check:importable", +]} +"check:ruff" = "ruff check ." +"fix:ruff" = "ruff check --fix ." + +"check:importable" = "python -c 'import cas_parser'" + +typecheck = { chain = [ + "typecheck:pyright", + "typecheck:mypy" +]} +"typecheck:pyright" = "pyright" +"typecheck:verify-types" = "pyright --verifytypes cas_parser --ignoreexternal" +"typecheck:mypy" = "mypy ." + +[build-system] +requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[tool.hatch.build] +include = [ + "src/*" +] + +[tool.hatch.build.targets.wheel] +packages = ["src/cas_parser"] + +[tool.hatch.build.targets.sdist] +# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc) +include = [ + "/*.toml", + "/*.json", + "/*.lock", + "/*.md", + "/mypy.ini", + "/noxfile.py", + "bin/*", + "examples/*", + "src/*", + "tests/*", +] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.md" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] +# replace relative links with absolute links +pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' +replacement = '[\1](https://github.com/stainless-sdks/cas-parser-python/tree/main/\g<2>)' + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "--tb=short -n auto" +xfail_strict = true +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" +filterwarnings = [ + "error" +] + +[tool.pyright] +# this enables practically every flag given by pyright. +# there are a couple of flags that are still disabled by +# default in strict mode as they are experimental and niche. +typeCheckingMode = "strict" +pythonVersion = "3.8" + +exclude = [ + "_dev", + ".venv", + ".nox", +] + +reportImplicitOverride = true +reportOverlappingOverload = false + +reportImportCycles = false +reportPrivateUsage = false + +[tool.ruff] +line-length = 120 +output-format = "grouped" +target-version = "py38" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = [ + # isort + "I", + # bugbear rules + "B", + # remove unused imports + "F401", + # bare except statements + "E722", + # unused arguments + "ARG", + # print statements + "T201", + "T203", + # misuse of typing.TYPE_CHECKING + "TC004", + # import rules + "TID251", +] +ignore = [ + # mutable defaults + "B006", +] +unfixable = [ + # disable auto fix for print statements + "T201", + "T203", +] + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" + +[tool.ruff.lint.isort] +length-sort = true +length-sort-straight = true +combine-as-imports = true +extra-standard-library = ["typing_extensions"] +known-first-party = ["cas_parser", "tests"] + +[tool.ruff.lint.per-file-ignores] +"bin/**.py" = ["T201", "T203"] +"scripts/**.py" = ["T201", "T203"] +"tests/**.py" = ["T201", "T203"] +"examples/**.py" = ["T201", "T203"] diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..ea03aeb --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,135 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: true +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.8 + # via cas-parser + # via httpx-aiohttp +aiosignal==1.3.2 + # via aiohttp +annotated-types==0.6.0 + # via pydantic +anyio==4.4.0 + # via cas-parser + # via httpx +argcomplete==3.1.2 + # via nox +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp +certifi==2023.7.22 + # via httpcore + # via httpx +colorlog==6.7.0 + # via nox +dirty-equals==0.6.0 +distlib==0.3.7 + # via virtualenv +distro==1.8.0 + # via cas-parser +exceptiongroup==1.2.2 + # via anyio + # via pytest +execnet==2.1.1 + # via pytest-xdist +filelock==3.12.4 + # via virtualenv +frozenlist==1.6.2 + # via aiohttp + # via aiosignal +h11==0.16.0 + # via httpcore +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via cas-parser + # via httpx-aiohttp + # via respx +httpx-aiohttp==0.1.8 + # via cas-parser +idna==3.4 + # via anyio + # via httpx + # via yarl +importlib-metadata==7.0.0 +iniconfig==2.0.0 + # via pytest +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +multidict==6.4.4 + # via aiohttp + # via yarl +mypy==1.14.1 +mypy-extensions==1.0.0 + # via mypy +nest-asyncio==1.6.0 +nodeenv==1.8.0 + # via pyright +nox==2023.4.22 +packaging==23.2 + # via nox + # via pytest +platformdirs==3.11.0 + # via virtualenv +pluggy==1.5.0 + # via pytest +propcache==0.3.1 + # via aiohttp + # via yarl +pydantic==2.10.3 + # via cas-parser +pydantic-core==2.27.1 + # via pydantic +pygments==2.18.0 + # via rich +pyright==1.1.399 +pytest==8.3.3 + # via pytest-asyncio + # via pytest-xdist +pytest-asyncio==0.24.0 +pytest-xdist==3.7.0 +python-dateutil==2.8.2 + # via time-machine +pytz==2023.3.post1 + # via dirty-equals +respx==0.22.0 +rich==13.7.1 +ruff==0.9.4 +setuptools==68.2.2 + # via nodeenv +six==1.16.0 + # via python-dateutil +sniffio==1.3.0 + # via anyio + # via cas-parser +time-machine==2.9.0 +tomli==2.0.2 + # via mypy + # via pytest +typing-extensions==4.12.2 + # via anyio + # via cas-parser + # via multidict + # via mypy + # via pydantic + # via pydantic-core + # via pyright +virtualenv==20.24.5 + # via nox +yarl==1.20.0 + # via aiohttp +zipp==3.17.0 + # via importlib-metadata diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..3de2b6b --- /dev/null +++ b/requirements.lock @@ -0,0 +1,72 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: true +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.8 + # via cas-parser + # via httpx-aiohttp +aiosignal==1.3.2 + # via aiohttp +annotated-types==0.6.0 + # via pydantic +anyio==4.4.0 + # via cas-parser + # via httpx +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp +certifi==2023.7.22 + # via httpcore + # via httpx +distro==1.8.0 + # via cas-parser +exceptiongroup==1.2.2 + # via anyio +frozenlist==1.6.2 + # via aiohttp + # via aiosignal +h11==0.16.0 + # via httpcore +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via cas-parser + # via httpx-aiohttp +httpx-aiohttp==0.1.8 + # via cas-parser +idna==3.4 + # via anyio + # via httpx + # via yarl +multidict==6.4.4 + # via aiohttp + # via yarl +propcache==0.3.1 + # via aiohttp + # via yarl +pydantic==2.10.3 + # via cas-parser +pydantic-core==2.27.1 + # via pydantic +sniffio==1.3.0 + # via anyio + # via cas-parser +typing-extensions==4.12.2 + # via anyio + # via cas-parser + # via multidict + # via pydantic + # via pydantic-core +yarl==1.20.0 + # via aiohttp diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 0000000..e84fe62 --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then + brew bundle check >/dev/null 2>&1 || { + echo "==> Installing Homebrew dependencies…" + brew bundle + } +fi + +echo "==> Installing Python dependencies…" + +# experimental uv support makes installations significantly faster +rye config --set-bool behavior.use-uv=true + +rye sync --all-features diff --git a/scripts/format b/scripts/format new file mode 100755 index 0000000..667ec2d --- /dev/null +++ b/scripts/format @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running formatters" +rye run format diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..d325f0b --- /dev/null +++ b/scripts/lint @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running lints" +rye run lint + +echo "==> Making sure it imports" +rye run python -c 'import cas_parser' diff --git a/scripts/mock b/scripts/mock new file mode 100755 index 0000000..0b28f6e --- /dev/null +++ b/scripts/mock @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [[ -n "$1" && "$1" != '--'* ]]; then + URL="$1" + shift +else + URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" +fi + +# Check if the URL is empty +if [ -z "$URL" ]; then + echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" + exit 1 +fi + +echo "==> Starting mock server with URL ${URL}" + +# Run prism mock on the given spec +if [ "$1" == "--daemon" ]; then + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + + # Wait for server to come online + echo -n "Waiting for server" + while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + echo -n "." + sleep 0.1 + done + + if grep -q "✖ fatal" ".prism.log"; then + cat .prism.log + exit 1 + fi + + echo +else + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" +fi diff --git a/scripts/test b/scripts/test new file mode 100755 index 0000000..dbeda2d --- /dev/null +++ b/scripts/test @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +function prism_is_running() { + curl --silent "http://localhost:4010" >/dev/null 2>&1 +} + +kill_server_on_port() { + pids=$(lsof -t -i tcp:"$1" || echo "") + if [ "$pids" != "" ]; then + kill "$pids" + echo "Stopped $pids." + fi +} + +function is_overriding_api_base_url() { + [ -n "$TEST_API_BASE_URL" ] +} + +if ! is_overriding_api_base_url && ! prism_is_running ; then + # When we exit this script, make sure to kill the background mock server process + trap 'kill_server_on_port 4010' EXIT + + # Start the dev server + ./scripts/mock --daemon +fi + +if is_overriding_api_base_url ; then + echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" + echo +elif ! prism_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" + echo -e "running against your OpenAPI spec." + echo + echo -e "To run the server, pass in the path or url of your OpenAPI" + echo -e "spec to the prism command:" + echo + echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo + + exit 1 +else + echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo +fi + +export DEFER_PYDANTIC_BUILD=false + +echo "==> Running tests" +rye run pytest "$@" + +echo "==> Running Pydantic v1 tests" +rye run nox -s test-pydantic-v1 -- "$@" diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py new file mode 100644 index 0000000..0cf2bd2 --- /dev/null +++ b/scripts/utils/ruffen-docs.py @@ -0,0 +1,167 @@ +# fork of https://github.com/asottile/blacken-docs adapted for ruff +from __future__ import annotations + +import re +import sys +import argparse +import textwrap +import contextlib +import subprocess +from typing import Match, Optional, Sequence, Generator, NamedTuple, cast + +MD_RE = re.compile( + r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", + re.DOTALL | re.MULTILINE, +) +MD_PYCON_RE = re.compile( + r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", + re.DOTALL | re.MULTILINE, +) +PYCON_PREFIX = ">>> " +PYCON_CONTINUATION_PREFIX = "..." +PYCON_CONTINUATION_RE = re.compile( + rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", +) +DEFAULT_LINE_LENGTH = 100 + + +class CodeBlockError(NamedTuple): + offset: int + exc: Exception + + +def format_str( + src: str, +) -> tuple[str, Sequence[CodeBlockError]]: + errors: list[CodeBlockError] = [] + + @contextlib.contextmanager + def _collect_error(match: Match[str]) -> Generator[None, None, None]: + try: + yield + except Exception as e: + errors.append(CodeBlockError(match.start(), e)) + + def _md_match(match: Match[str]) -> str: + code = textwrap.dedent(match["code"]) + with _collect_error(match): + code = format_code_block(code) + code = textwrap.indent(code, match["indent"]) + return f"{match['before']}{code}{match['after']}" + + def _pycon_match(match: Match[str]) -> str: + code = "" + fragment = cast(Optional[str], None) + + def finish_fragment() -> None: + nonlocal code + nonlocal fragment + + if fragment is not None: + with _collect_error(match): + fragment = format_code_block(fragment) + fragment_lines = fragment.splitlines() + code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" + for line in fragment_lines[1:]: + # Skip blank lines to handle Black adding a blank above + # functions within blocks. A blank line would end the REPL + # continuation prompt. + # + # >>> if True: + # ... def f(): + # ... pass + # ... + if line: + code += f"{PYCON_CONTINUATION_PREFIX} {line}\n" + if fragment_lines[-1].startswith(" "): + code += f"{PYCON_CONTINUATION_PREFIX}\n" + fragment = None + + indentation = None + for line in match["code"].splitlines(): + orig_line, line = line, line.lstrip() + if indentation is None and line: + indentation = len(orig_line) - len(line) + continuation_match = PYCON_CONTINUATION_RE.match(line) + if continuation_match and fragment is not None: + fragment += line[continuation_match.end() :] + "\n" + else: + finish_fragment() + if line.startswith(PYCON_PREFIX): + fragment = line[len(PYCON_PREFIX) :] + "\n" + else: + code += orig_line[indentation:] + "\n" + finish_fragment() + return code + + def _md_pycon_match(match: Match[str]) -> str: + code = _pycon_match(match) + code = textwrap.indent(code, match["indent"]) + return f"{match['before']}{code}{match['after']}" + + src = MD_RE.sub(_md_match, src) + src = MD_PYCON_RE.sub(_md_pycon_match, src) + return src, errors + + +def format_code_block(code: str) -> str: + return subprocess.check_output( + [ + sys.executable, + "-m", + "ruff", + "format", + "--stdin-filename=script.py", + f"--line-length={DEFAULT_LINE_LENGTH}", + ], + encoding="utf-8", + input=code, + ) + + +def format_file( + filename: str, + skip_errors: bool, +) -> int: + with open(filename, encoding="UTF-8") as f: + contents = f.read() + new_contents, errors = format_str(contents) + for error in errors: + lineno = contents[: error.offset].count("\n") + 1 + print(f"{filename}:{lineno}: code block parse error {error.exc}") + if errors and not skip_errors: + return 1 + if contents != new_contents: + print(f"{filename}: Rewriting...") + with open(filename, "w", encoding="UTF-8") as f: + f.write(new_contents) + return 0 + else: + return 0 + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "-l", + "--line-length", + type=int, + default=DEFAULT_LINE_LENGTH, + ) + parser.add_argument( + "-S", + "--skip-string-normalization", + action="store_true", + ) + parser.add_argument("-E", "--skip-errors", action="store_true") + parser.add_argument("filenames", nargs="*") + args = parser.parse_args(argv) + + retv = 0 + for filename in args.filenames: + retv |= format_file(filename, skip_errors=args.skip_errors) + return retv + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh new file mode 100755 index 0000000..073cc2d --- /dev/null +++ b/scripts/utils/upload-artifact.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -exuo pipefail + +FILENAME=$(basename dist/*.whl) + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + +SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') + +if [[ "$SIGNED_URL" == "null" ]]; then + echo -e "\033[31mFailed to get signed URL.\033[0m" + exit 1 +fi + +UPLOAD_RESPONSE=$(curl -v -X PUT \ + -H "Content-Type: binary/octet-stream" \ + --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) + +if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then + echo -e "\033[32mUploaded build to Stainless storage.\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/cas-parser-python/$SHA/$FILENAME'\033[0m" +else + echo -e "\033[31mFailed to upload artifact.\033[0m" + exit 1 +fi diff --git a/src/cas_parser/__init__.py b/src/cas_parser/__init__.py new file mode 100644 index 0000000..25d271f --- /dev/null +++ b/src/cas_parser/__init__.py @@ -0,0 +1,102 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import typing as _t + +from . import types +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes +from ._utils import file_from_path +from ._client import ( + ENVIRONMENTS, + Client, + Stream, + Timeout, + CasParser, + Transport, + AsyncClient, + AsyncStream, + AsyncCasParser, + RequestOptions, +) +from ._models import BaseModel +from ._version import __title__, __version__ +from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse +from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS +from ._exceptions import ( + APIError, + ConflictError, + NotFoundError, + APIStatusError, + CasParserError, + RateLimitError, + APITimeoutError, + BadRequestError, + APIConnectionError, + AuthenticationError, + InternalServerError, + PermissionDeniedError, + UnprocessableEntityError, + APIResponseValidationError, +) +from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient +from ._utils._logs import setup_logging as _setup_logging + +__all__ = [ + "types", + "__version__", + "__title__", + "NoneType", + "Transport", + "ProxiesTypes", + "NotGiven", + "NOT_GIVEN", + "Omit", + "CasParserError", + "APIError", + "APIStatusError", + "APITimeoutError", + "APIConnectionError", + "APIResponseValidationError", + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", + "Timeout", + "RequestOptions", + "Client", + "AsyncClient", + "Stream", + "AsyncStream", + "CasParser", + "AsyncCasParser", + "ENVIRONMENTS", + "file_from_path", + "BaseModel", + "DEFAULT_TIMEOUT", + "DEFAULT_MAX_RETRIES", + "DEFAULT_CONNECTION_LIMITS", + "DefaultHttpxClient", + "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", +] + +if not _t.TYPE_CHECKING: + from ._utils._resources_proxy import resources as resources + +_setup_logging() + +# Update the __module__ attribute for exported symbols so that +# error messages point to this module instead of the module +# it was originally defined in, e.g. +# cas_parser._exceptions.NotFoundError -> cas_parser.NotFoundError +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + try: + __locals[__name].__module__ = "cas_parser" + except (TypeError, AttributeError): + # Some of our exported symbols are builtins which we can't set attributes for. + pass diff --git a/src/cas_parser/_base_client.py b/src/cas_parser/_base_client.py new file mode 100644 index 0000000..c41565d --- /dev/null +++ b/src/cas_parser/_base_client.py @@ -0,0 +1,1995 @@ +from __future__ import annotations + +import sys +import json +import time +import uuid +import email +import asyncio +import inspect +import logging +import platform +import email.utils +from types import TracebackType +from random import random +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Type, + Union, + Generic, + Mapping, + TypeVar, + Iterable, + Iterator, + Optional, + Generator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Literal, override, get_origin + +import anyio +import httpx +import distro +import pydantic +from httpx import URL +from pydantic import PrivateAttr + +from . import _exceptions +from ._qs import Querystring +from ._files import to_httpx_files, async_to_httpx_files +from ._types import ( + NOT_GIVEN, + Body, + Omit, + Query, + Headers, + Timeout, + NotGiven, + ResponseT, + AnyMapping, + PostParser, + RequestFiles, + HttpxSendArgs, + RequestOptions, + HttpxRequestFiles, + ModelBuilderProtocol, +) +from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping +from ._compat import PYDANTIC_V2, model_copy, model_dump +from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type +from ._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + extract_response_type, +) +from ._constants import ( + DEFAULT_TIMEOUT, + MAX_RETRY_DELAY, + DEFAULT_MAX_RETRIES, + INITIAL_RETRY_DELAY, + RAW_RESPONSE_HEADER, + OVERRIDE_CAST_TO_HEADER, + DEFAULT_CONNECTION_LIMITS, +) +from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder +from ._exceptions import ( + APIStatusError, + APITimeoutError, + APIConnectionError, + APIResponseValidationError, +) + +log: logging.Logger = logging.getLogger(__name__) + +# TODO: make base page type vars covariant +SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]") +AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]") + + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) + +_StreamT = TypeVar("_StreamT", bound=Stream[Any]) +_AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any]) + +if TYPE_CHECKING: + from httpx._config import ( + DEFAULT_TIMEOUT_CONFIG, # pyright: ignore[reportPrivateImportUsage] + ) + + HTTPX_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT_CONFIG +else: + try: + from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT + except ImportError: + # taken from https://github.com/encode/httpx/blob/3ba5fe0d7ac70222590e759c31442b1cab263791/httpx/_config.py#L366 + HTTPX_DEFAULT_TIMEOUT = Timeout(5.0) + + +class PageInfo: + """Stores the necessary information to build the request to retrieve the next page. + + Either `url` or `params` must be set. + """ + + url: URL | NotGiven + params: Query | NotGiven + json: Body | NotGiven + + @overload + def __init__( + self, + *, + url: URL, + ) -> None: ... + + @overload + def __init__( + self, + *, + params: Query, + ) -> None: ... + + @overload + def __init__( + self, + *, + json: Body, + ) -> None: ... + + def __init__( + self, + *, + url: URL | NotGiven = NOT_GIVEN, + json: Body | NotGiven = NOT_GIVEN, + params: Query | NotGiven = NOT_GIVEN, + ) -> None: + self.url = url + self.json = json + self.params = params + + @override + def __repr__(self) -> str: + if self.url: + return f"{self.__class__.__name__}(url={self.url})" + if self.json: + return f"{self.__class__.__name__}(json={self.json})" + return f"{self.__class__.__name__}(params={self.params})" + + +class BasePage(GenericModel, Generic[_T]): + """ + Defines the core interface for pagination. + + Type Args: + ModelT: The pydantic model that represents an item in the response. + + Methods: + has_next_page(): Check if there is another page available + next_page_info(): Get the necessary information to make a request for the next page + """ + + _options: FinalRequestOptions = PrivateAttr() + _model: Type[_T] = PrivateAttr() + + def has_next_page(self) -> bool: + items = self._get_page_items() + if not items: + return False + return self.next_page_info() is not None + + def next_page_info(self) -> Optional[PageInfo]: ... + + def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body] + ... + + def _params_from_url(self, url: URL) -> httpx.QueryParams: + # TODO: do we have to preprocess params here? + return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params) + + def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: + options = model_copy(self._options) + options._strip_raw_response_header() + + if not isinstance(info.params, NotGiven): + options.params = {**options.params, **info.params} + return options + + if not isinstance(info.url, NotGiven): + params = self._params_from_url(info.url) + url = info.url.copy_with(params=params) + options.params = dict(url.params) + options.url = str(url) + return options + + if not isinstance(info.json, NotGiven): + if not is_mapping(info.json): + raise TypeError("Pagination is only supported with mappings") + + if not options.json_data: + options.json_data = {**info.json} + else: + if not is_mapping(options.json_data): + raise TypeError("Pagination is only supported with mappings") + + options.json_data = {**options.json_data, **info.json} + return options + + raise ValueError("Unexpected PageInfo state") + + +class BaseSyncPage(BasePage[_T], Generic[_T]): + _client: SyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + client: SyncAPIClient, + model: Type[_T], + options: FinalRequestOptions, + ) -> None: + if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + + self._model = model + self._client = client + self._options = options + + # Pydantic uses a custom `__iter__` method to support casting BaseModels + # to dictionaries. e.g. dict(model). + # As we want to support `for item in page`, this is inherently incompatible + # with the default pydantic behaviour. It is not possible to support both + # use cases at once. Fortunately, this is not a big deal as all other pydantic + # methods should continue to work as expected as there is an alternative method + # to cast a model to a dictionary, model.dict(), which is used internally + # by pydantic. + def __iter__(self) -> Iterator[_T]: # type: ignore + for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = page.get_next_page() + else: + return + + def get_next_page(self: SyncPageT) -> SyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return self._client._request_api_list(self._model, page=self.__class__, options=options) + + +class AsyncPaginator(Generic[_T, AsyncPageT]): + def __init__( + self, + client: AsyncAPIClient, + options: FinalRequestOptions, + page_cls: Type[AsyncPageT], + model: Type[_T], + ) -> None: + self._model = model + self._client = client + self._options = options + self._page_cls = page_cls + + def __await__(self) -> Generator[Any, None, AsyncPageT]: + return self._get_page().__await__() + + async def _get_page(self) -> AsyncPageT: + def _parser(resp: AsyncPageT) -> AsyncPageT: + resp._set_private_attributes( + model=self._model, + options=self._options, + client=self._client, + ) + return resp + + self._options.post_parser = _parser + + return await self._client.request(self._page_cls, self._options) + + async def __aiter__(self) -> AsyncIterator[_T]: + # https://github.com/microsoft/pyright/issues/3464 + page = cast( + AsyncPageT, + await self, # type: ignore + ) + async for item in page: + yield item + + +class BaseAsyncPage(BasePage[_T], Generic[_T]): + _client: AsyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + model: Type[_T], + client: AsyncAPIClient, + options: FinalRequestOptions, + ) -> None: + if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + + self._model = model + self._client = client + self._options = options + + async def __aiter__(self) -> AsyncIterator[_T]: + async for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = await page.get_next_page() + else: + return + + async def get_next_page(self: AsyncPageT) -> AsyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return await self._client._request_api_list(self._model, page=self.__class__, options=options) + + +_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) +_DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) + + +class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): + _client: _HttpxClientT + _version: str + _base_url: URL + max_retries: int + timeout: Union[float, Timeout, None] + _strict_response_validation: bool + _idempotency_header: str | None + _default_stream_cls: type[_DefaultStreamT] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None = DEFAULT_TIMEOUT, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + self._version = version + self._base_url = self._enforce_trailing_slash(URL(base_url)) + self.max_retries = max_retries + self.timeout = timeout + self._custom_headers = custom_headers or {} + self._custom_query = custom_query or {} + self._strict_response_validation = _strict_response_validation + self._idempotency_header = None + self._platform: Platform | None = None + + if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] + raise TypeError( + "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `cas_parser.DEFAULT_MAX_RETRIES`" + ) + + def _enforce_trailing_slash(self, url: URL) -> URL: + if url.raw_path.endswith(b"/"): + return url + return url.copy_with(raw_path=url.raw_path + b"/") + + def _make_status_error_from_response( + self, + response: httpx.Response, + ) -> APIStatusError: + if response.is_closed and not response.is_stream_consumed: + # We can't read the response body as it has been closed + # before it was read. This can happen if an event hook + # raises a status error. + body = None + err_msg = f"Error code: {response.status_code}" + else: + err_text = response.text.strip() + body = err_text + + try: + body = json.loads(err_text) + err_msg = f"Error code: {response.status_code} - {body}" + except Exception: + err_msg = err_text or f"Error code: {response.status_code}" + + return self._make_status_error(err_msg, body=body, response=response) + + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> _exceptions.APIStatusError: + raise NotImplementedError() + + def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: + custom_headers = options.headers or {} + headers_dict = _merge_mappings(self.default_headers, custom_headers) + self._validate_headers(headers_dict, custom_headers) + + # headers are case-insensitive while dictionaries are not. + headers = httpx.Headers(headers_dict) + + idempotency_header = self._idempotency_header + if idempotency_header and options.idempotency_key and idempotency_header not in headers: + headers[idempotency_header] = options.idempotency_key + + # Don't set these headers if they were already set or removed by the caller. We check + # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. + lower_custom_headers = [header.lower() for header in custom_headers] + if "x-stainless-retry-count" not in lower_custom_headers: + headers["x-stainless-retry-count"] = str(retries_taken) + if "x-stainless-read-timeout" not in lower_custom_headers: + timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout + if isinstance(timeout, Timeout): + timeout = timeout.read + if timeout is not None: + headers["x-stainless-read-timeout"] = str(timeout) + + return headers + + def _prepare_url(self, url: str) -> URL: + """ + Merge a URL argument together with any 'base_url' on the client, + to create the URL used for the outgoing request. + """ + # Copied from httpx's `_merge_url` method. + merge_url = URL(url) + if merge_url.is_relative_url: + merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") + return self.base_url.copy_with(raw_path=merge_raw_path) + + return merge_url + + def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder: + return SSEDecoder() + + def _build_request( + self, + options: FinalRequestOptions, + *, + retries_taken: int = 0, + ) -> httpx.Request: + if log.isEnabledFor(logging.DEBUG): + log.debug("Request options: %s", model_dump(options, exclude_unset=True)) + + kwargs: dict[str, Any] = {} + + json_data = options.json_data + if options.extra_json is not None: + if json_data is None: + json_data = cast(Body, options.extra_json) + elif is_mapping(json_data): + json_data = _merge_mappings(json_data, options.extra_json) + else: + raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") + + headers = self._build_headers(options, retries_taken=retries_taken) + params = _merge_mappings(self.default_query, options.params) + content_type = headers.get("Content-Type") + files = options.files + + # If the given Content-Type header is multipart/form-data then it + # has to be removed so that httpx can generate the header with + # additional information for us as it has to be in this form + # for the server to be able to correctly parse the request: + # multipart/form-data; boundary=---abc-- + if content_type is not None and content_type.startswith("multipart/form-data"): + if "boundary" not in content_type: + # only remove the header if the boundary hasn't been explicitly set + # as the caller doesn't want httpx to come up with their own boundary + headers.pop("Content-Type") + + # As we are now sending multipart/form-data instead of application/json + # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding + if json_data: + if not is_dict(json_data): + raise TypeError( + f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead." + ) + kwargs["data"] = self._serialize_multipartform(json_data) + + # httpx determines whether or not to send a "multipart/form-data" + # request based on the truthiness of the "files" argument. + # This gets around that issue by generating a dict value that + # evaluates to true. + # + # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186 + if not files: + files = cast(HttpxRequestFiles, ForceMultipartDict()) + + prepared_url = self._prepare_url(options.url) + if "_" in prepared_url.host: + # work around https://github.com/encode/httpx/discussions/2880 + kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + + is_body_allowed = options.method.lower() != "get" + + if is_body_allowed: + if isinstance(json_data, bytes): + kwargs["content"] = json_data + else: + kwargs["json"] = json_data if is_given(json_data) else None + kwargs["files"] = files + else: + headers.pop("Content-Type", None) + kwargs.pop("data", None) + + # TODO: report this error to httpx + return self._client.build_request( # pyright: ignore[reportUnknownMemberType] + headers=headers, + timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, + method=options.method, + url=prepared_url, + # the `Query` type that we use is incompatible with qs' + # `Params` type as it needs to be typed as `Mapping[str, object]` + # so that passing a `TypedDict` doesn't cause an error. + # https://github.com/microsoft/pyright/issues/3526#event-6715453066 + params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, + **kwargs, + ) + + def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]: + items = self.qs.stringify_items( + # TODO: type ignore is required as stringify_items is well typed but we can't be + # well typed without heavy validation. + data, # type: ignore + array_format="brackets", + ) + serialized: dict[str, object] = {} + for key, value in items: + existing = serialized.get(key) + + if not existing: + serialized[key] = value + continue + + # If a value has already been set for this key then that + # means we're sending data like `array[]=[1, 2, 3]` and we + # need to tell httpx that we want to send multiple values with + # the same key which is done by using a list or a tuple. + # + # Note: 2d arrays should never result in the same key at both + # levels so it's safe to assume that if the value is a list, + # it was because we changed it to be a list. + if is_list(existing): + existing.append(value) + else: + serialized[key] = [existing, value] + + return serialized + + def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]: + if not is_given(options.headers): + return cast_to + + # make a copy of the headers so we don't mutate user-input + headers = dict(options.headers) + + # we internally support defining a temporary header to override the + # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` + # see _response.py for implementation details + override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN) + if is_given(override_cast_to): + options.headers = headers + return cast(Type[ResponseT], override_cast_to) + + return cast_to + + def _should_stream_response_body(self, request: httpx.Request) -> bool: + return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return] + + def _process_response_data( + self, + *, + data: object, + cast_to: type[ResponseT], + response: httpx.Response, + ) -> ResponseT: + if data is None: + return cast(ResponseT, None) + + if cast_to is object: + return cast(ResponseT, data) + + try: + if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol): + return cast(ResponseT, cast_to.build(response=response, data=data)) + + if self._strict_response_validation: + return cast(ResponseT, validate_type(type_=cast_to, value=data)) + + return cast(ResponseT, construct_type(type_=cast_to, value=data)) + except pydantic.ValidationError as err: + raise APIResponseValidationError(response=response, body=data) from err + + @property + def qs(self) -> Querystring: + return Querystring() + + @property + def custom_auth(self) -> httpx.Auth | None: + return None + + @property + def auth_headers(self) -> dict[str, str]: + return {} + + @property + def default_headers(self) -> dict[str, str | Omit]: + return { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": self.user_agent, + **self.platform_headers(), + **self.auth_headers, + **self._custom_headers, + } + + @property + def default_query(self) -> dict[str, object]: + return { + **self._custom_query, + } + + def _validate_headers( + self, + headers: Headers, # noqa: ARG002 + custom_headers: Headers, # noqa: ARG002 + ) -> None: + """Validate the given default headers and custom headers. + + Does nothing by default. + """ + return + + @property + def user_agent(self) -> str: + return f"{self.__class__.__name__}/Python {self._version}" + + @property + def base_url(self) -> URL: + return self._base_url + + @base_url.setter + def base_url(self, url: URL | str) -> None: + self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) + + def platform_headers(self) -> Dict[str, str]: + # the actual implementation is in a separate `lru_cache` decorated + # function because adding `lru_cache` to methods will leak memory + # https://github.com/python/cpython/issues/88476 + return platform_headers(self._version, platform=self._platform) + + def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: + """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. + + About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax + """ + if response_headers is None: + return None + + # First, try the non-standard `retry-after-ms` header for milliseconds, + # which is more precise than integer-seconds `retry-after` + try: + retry_ms_header = response_headers.get("retry-after-ms", None) + return float(retry_ms_header) / 1000 + except (TypeError, ValueError): + pass + + # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). + retry_header = response_headers.get("retry-after") + try: + # note: the spec indicates that this should only ever be an integer + # but if someone sends a float there's no reason for us to not respect it + return float(retry_header) + except (TypeError, ValueError): + pass + + # Last, try parsing `retry-after` as a date. + retry_date_tuple = email.utils.parsedate_tz(retry_header) + if retry_date_tuple is None: + return None + + retry_date = email.utils.mktime_tz(retry_date_tuple) + return float(retry_date - time.time()) + + def _calculate_retry_timeout( + self, + remaining_retries: int, + options: FinalRequestOptions, + response_headers: Optional[httpx.Headers] = None, + ) -> float: + max_retries = options.get_max_retries(self.max_retries) + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = self._parse_retry_after_header(response_headers) + if retry_after is not None and 0 < retry_after <= 60: + return retry_after + + # Also cap retry count to 1000 to avoid any potential overflows with `pow` + nb_retries = min(max_retries - remaining_retries, 1000) + + # Apply exponential backoff, but not more than the max. + sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) + + # Apply some jitter, plus-or-minus half a second. + jitter = 1 - 0.25 * random() + timeout = sleep_seconds * jitter + return timeout if timeout >= 0 else 0 + + def _should_retry(self, response: httpx.Response) -> bool: + # Note: this is not a standard header + should_retry_header = response.headers.get("x-should-retry") + + # If the server explicitly says whether or not to retry, obey. + if should_retry_header == "true": + log.debug("Retrying as header `x-should-retry` is set to `true`") + return True + if should_retry_header == "false": + log.debug("Not retrying as header `x-should-retry` is set to `false`") + return False + + # Retry on request timeouts. + if response.status_code == 408: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on lock timeouts. + if response.status_code == 409: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on rate limits. + if response.status_code == 429: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry internal errors. + if response.status_code >= 500: + log.debug("Retrying due to status code %i", response.status_code) + return True + + log.debug("Not retrying") + return False + + def _idempotency_key(self) -> str: + return f"stainless-python-retry-{uuid.uuid4()}" + + +class _DefaultHttpxClient(httpx.Client): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultHttpxClient = httpx.Client + """An alias to `httpx.Client` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.Client` will result in httpx's defaults being used, not ours. + """ +else: + DefaultHttpxClient = _DefaultHttpxClient + + +class SyncHttpxClientWrapper(DefaultHttpxClient): + def __del__(self) -> None: + if self.is_closed: + return + + try: + self.close() + except Exception: + pass + + +class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]): + _client: httpx.Client + _default_stream_cls: type[Stream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + http_client: httpx.Client | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + _strict_response_validation: bool, + ) -> None: + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.Client` but got {type(http_client)}" + ) + + super().__init__( + version=version, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + base_url=base_url, + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or SyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + # If an error is thrown while constructing a client, self._client + # may not be present + if hasattr(self, "_client"): + self._client.close() + + def __enter__(self: _T) -> _T: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> FinalRequestOptions: + """Hook for mutating the given options""" + return options + + def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[True], + stream_cls: Type[_StreamT], + ) -> _StreamT: ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: Type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + cast_to = self._maybe_override_cast_to(cast_to, options) + + # create a copy of the options we were given so that if the + # options are mutated later & we then retry, the retries are + # given the original options + input_options = model_copy(options) + if input_options.idempotency_key is None and input_options.method.lower() != "get": + # ensure the idempotency key is reused between requests + input_options.idempotency_key = self._idempotency_key() + + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) + + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = self._prepare_options(options) + + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth + + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + + log.debug("Sending HTTP Request: %s %s", request.method, request.url) + + response = None + try: + response = self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + err.response.close() + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + err.response.read() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + break + + assert response is not None, "could not resolve response (should never happen)" + return self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + retries_taken=retries_taken, + ) + + def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken + if remaining_retries == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining_retries) + + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + time.sleep(timeout) + + def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): + if not issubclass(origin, APIResponse): + raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + ResponseT, + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = APIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return api_response.parse() + + def _request_api_list( + self, + model: Type[object], + page: Type[SyncPageT], + options: FinalRequestOptions, + ) -> SyncPageT: + def _parser(resp: SyncPageT) -> SyncPageT: + resp._set_private_attributes( + client=self, + model=model, + options=options, + ) + return resp + + options.post_parser = _parser + + return self.request(page, options, stream=False) + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + # cast is required because mypy complains about returning Any even though + # it understands the type variables + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=to_httpx_files(files), **options + ) + return self.request(cast_to, opts) + + def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + return self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[object], + page: Type[SyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> SyncPageT: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +class _DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +try: + import httpx_aiohttp +except ImportError: + + class _DefaultAioHttpClient(httpx.AsyncClient): + def __init__(self, **_kwargs: Any) -> None: + raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") +else: + + class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultAsyncHttpxClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.AsyncClient` will result in httpx's defaults being used, not ours. + """ + + DefaultAioHttpClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" +else: + DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + DefaultAioHttpClient = _DefaultAioHttpClient + + +class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): + def __del__(self) -> None: + if self.is_closed: + return + + try: + # TODO(someday): support non asyncio runtimes here + asyncio.get_running_loop().create_task(self.aclose()) + except Exception: + pass + + +class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]): + _client: httpx.AsyncClient + _default_stream_cls: type[AsyncStream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + http_client: httpx.AsyncClient | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.AsyncClient` but got {type(http_client)}" + ) + + super().__init__( + version=version, + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or AsyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + async def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + await self._client.aclose() + + async def __aenter__(self: _T) -> _T: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> FinalRequestOptions: + """Hook for mutating the given options""" + return options + + async def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + if self._platform is None: + # `get_platform` can make blocking IO calls so we + # execute it earlier while we are in an async context + self._platform = await asyncify(get_platform)() + + cast_to = self._maybe_override_cast_to(cast_to, options) + + # create a copy of the options we were given so that if the + # options are mutated later & we then retry, the retries are + # given the original options + input_options = model_copy(options) + if input_options.idempotency_key is None and input_options.method.lower() != "get": + # ensure the idempotency key is reused between requests + input_options.idempotency_key = self._idempotency_key() + + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) + + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = await self._prepare_options(options) + + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + await self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth + + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + + log.debug("Sending HTTP Request: %s %s", request.method, request.url) + + response = None + try: + response = await self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + await err.response.aclose() + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + await err.response.aread() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + break + + assert response is not None, "could not resolve response (should never happen)" + return await self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + retries_taken=retries_taken, + ) + + async def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken + if remaining_retries == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining_retries) + + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + await anyio.sleep(timeout) + + async def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): + if not issubclass(origin, AsyncAPIResponse): + raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + "ResponseT", + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = AsyncAPIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return await api_response.parse() + + def _request_api_list( + self, + model: Type[_T], + page: Type[AsyncPageT], + options: FinalRequestOptions, + ) -> AsyncPaginator[_T, AsyncPageT]: + return AsyncPaginator(client=self, options=options, page_cls=page, model=model) + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + async def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + return await self.request(cast_to, opts) + + async def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts) + + async def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + return await self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[_T], + page: Type[AsyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> AsyncPaginator[_T, AsyncPageT]: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +def make_request_options( + *, + query: Query | None = None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + post_parser: PostParser | NotGiven = NOT_GIVEN, +) -> RequestOptions: + """Create a dict of type RequestOptions without keys of NotGiven values.""" + options: RequestOptions = {} + if extra_headers is not None: + options["headers"] = extra_headers + + if extra_body is not None: + options["extra_json"] = cast(AnyMapping, extra_body) + + if query is not None: + options["params"] = query + + if extra_query is not None: + options["params"] = {**options.get("params", {}), **extra_query} + + if not isinstance(timeout, NotGiven): + options["timeout"] = timeout + + if idempotency_key is not None: + options["idempotency_key"] = idempotency_key + + if is_given(post_parser): + # internal + options["post_parser"] = post_parser # type: ignore + + return options + + +class ForceMultipartDict(Dict[str, None]): + def __bool__(self) -> bool: + return True + + +class OtherPlatform: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"Other:{self.name}" + + +Platform = Union[ + OtherPlatform, + Literal[ + "MacOS", + "Linux", + "Windows", + "FreeBSD", + "OpenBSD", + "iOS", + "Android", + "Unknown", + ], +] + + +def get_platform() -> Platform: + try: + system = platform.system().lower() + platform_name = platform.platform().lower() + except Exception: + return "Unknown" + + if "iphone" in platform_name or "ipad" in platform_name: + # Tested using Python3IDE on an iPhone 11 and Pythonista on an iPad 7 + # system is Darwin and platform_name is a string like: + # - Darwin-21.6.0-iPhone12,1-64bit + # - Darwin-21.6.0-iPad7,11-64bit + return "iOS" + + if system == "darwin": + return "MacOS" + + if system == "windows": + return "Windows" + + if "android" in platform_name: + # Tested using Pydroid 3 + # system is Linux and platform_name is a string like 'Linux-5.10.81-android12-9-00001-geba40aecb3b7-ab8534902-aarch64-with-libc' + return "Android" + + if system == "linux": + # https://distro.readthedocs.io/en/latest/#distro.id + distro_id = distro.id() + if distro_id == "freebsd": + return "FreeBSD" + + if distro_id == "openbsd": + return "OpenBSD" + + return "Linux" + + if platform_name: + return OtherPlatform(platform_name) + + return "Unknown" + + +@lru_cache(maxsize=None) +def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]: + return { + "X-Stainless-Lang": "python", + "X-Stainless-Package-Version": version, + "X-Stainless-OS": str(platform or get_platform()), + "X-Stainless-Arch": str(get_architecture()), + "X-Stainless-Runtime": get_python_runtime(), + "X-Stainless-Runtime-Version": get_python_version(), + } + + +class OtherArch: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"other:{self.name}" + + +Arch = Union[OtherArch, Literal["x32", "x64", "arm", "arm64", "unknown"]] + + +def get_python_runtime() -> str: + try: + return platform.python_implementation() + except Exception: + return "unknown" + + +def get_python_version() -> str: + try: + return platform.python_version() + except Exception: + return "unknown" + + +def get_architecture() -> Arch: + try: + machine = platform.machine().lower() + except Exception: + return "unknown" + + if machine in ("arm64", "aarch64"): + return "arm64" + + # TODO: untested + if machine == "arm": + return "arm" + + if machine == "x86_64": + return "x64" + + # TODO: untested + if sys.maxsize <= 2**32: + return "x32" + + if machine: + return OtherArch(machine) + + return "unknown" + + +def _merge_mappings( + obj1: Mapping[_T_co, Union[_T, Omit]], + obj2: Mapping[_T_co, Union[_T, Omit]], +) -> Dict[_T_co, _T]: + """Merge two mappings of the same type, removing any values that are instances of `Omit`. + + In cases with duplicate keys the second mapping takes precedence. + """ + merged = {**obj1, **obj2} + return {key: value for key, value in merged.items() if not isinstance(value, Omit)} diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py new file mode 100644 index 0000000..2bd3fc9 --- /dev/null +++ b/src/cas_parser/_client.py @@ -0,0 +1,469 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, Dict, Union, Mapping, cast +from typing_extensions import Self, Literal, override + +import httpx + +from . import _exceptions +from ._qs import Querystring +from ._types import ( + NOT_GIVEN, + Omit, + Timeout, + NotGiven, + Transport, + ProxiesTypes, + RequestOptions, +) +from ._utils import is_given, get_async_library +from ._version import __version__ +from .resources import cas_parser, cas_generator +from ._streaming import Stream as Stream, AsyncStream as AsyncStream +from ._exceptions import APIStatusError, CasParserError +from ._base_client import ( + DEFAULT_MAX_RETRIES, + SyncAPIClient, + AsyncAPIClient, +) + +__all__ = [ + "ENVIRONMENTS", + "Timeout", + "Transport", + "ProxiesTypes", + "RequestOptions", + "CasParser", + "AsyncCasParser", + "Client", + "AsyncClient", +] + +ENVIRONMENTS: Dict[str, str] = { + "production": "https://portfolio-parser.api.casparser.in", + "local": "http://localhost:5000", +} + + +class CasParser(SyncAPIClient): + cas_parser: cas_parser.CasParserResource + cas_generator: cas_generator.CasGeneratorResource + with_raw_response: CasParserWithRawResponse + with_streaming_response: CasParserWithStreamedResponse + + # client options + api_key: str + + _environment: Literal["production", "local"] | NotGiven + + def __init__( + self, + *, + api_key: str | None = None, + environment: Literal["production", "local"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, + timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + # Configure a custom httpx client. + # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details. + http_client: httpx.Client | None = None, + # Enable or disable schema validation for data returned by the API. + # When enabled an error APIResponseValidationError is raised + # if the API responds with invalid data for the expected schema. + # + # This parameter may be removed or changed in the future. + # If you rely on this feature, please open a GitHub issue + # outlining your use-case to help us decide if it should be + # part of our public interface in the future. + _strict_response_validation: bool = False, + ) -> None: + """Construct a new synchronous CasParser client instance. + + This automatically infers the `api_key` argument from the `CAS_PARSER_API_KEY` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("CAS_PARSER_API_KEY") + if api_key is None: + raise CasParserError( + "The api_key client option must be set either by passing api_key to the client or by setting the CAS_PARSER_API_KEY environment variable" + ) + self.api_key = api_key + + self._environment = environment + + base_url_env = os.environ.get("CAS_PARSER_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `CAS_PARSER_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + + super().__init__( + version=__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + + self.cas_parser = cas_parser.CasParserResource(self) + self.cas_generator = cas_generator.CasGeneratorResource(self) + self.with_raw_response = CasParserWithRawResponse(self) + self.with_streaming_response = CasParserWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + api_key = self.api_key + return {"x-api-key": api_key} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": "false", + **self._custom_headers, + } + + def copy( + self, + *, + api_key: str | None = None, + environment: Literal["production", "local"] | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + http_client: httpx.Client | None = None, + max_retries: int | NotGiven = NOT_GIVEN, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + http_client = http_client or self._client + return self.__class__( + api_key=api_key or self.api_key, + base_url=base_url or self.base_url, + environment=environment or self._environment, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, + ) + + # Alias for `copy` for nicer inline usage, e.g. + # client.with_options(timeout=10).foo.create(...) + with_options = copy + + @override + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> APIStatusError: + if response.status_code == 400: + return _exceptions.BadRequestError(err_msg, response=response, body=body) + + if response.status_code == 401: + return _exceptions.AuthenticationError(err_msg, response=response, body=body) + + if response.status_code == 403: + return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) + + if response.status_code == 404: + return _exceptions.NotFoundError(err_msg, response=response, body=body) + + if response.status_code == 409: + return _exceptions.ConflictError(err_msg, response=response, body=body) + + if response.status_code == 422: + return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) + + if response.status_code == 429: + return _exceptions.RateLimitError(err_msg, response=response, body=body) + + if response.status_code >= 500: + return _exceptions.InternalServerError(err_msg, response=response, body=body) + return APIStatusError(err_msg, response=response, body=body) + + +class AsyncCasParser(AsyncAPIClient): + cas_parser: cas_parser.AsyncCasParserResource + cas_generator: cas_generator.AsyncCasGeneratorResource + with_raw_response: AsyncCasParserWithRawResponse + with_streaming_response: AsyncCasParserWithStreamedResponse + + # client options + api_key: str + + _environment: Literal["production", "local"] | NotGiven + + def __init__( + self, + *, + api_key: str | None = None, + environment: Literal["production", "local"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, + timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + # Configure a custom httpx client. + # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details. + http_client: httpx.AsyncClient | None = None, + # Enable or disable schema validation for data returned by the API. + # When enabled an error APIResponseValidationError is raised + # if the API responds with invalid data for the expected schema. + # + # This parameter may be removed or changed in the future. + # If you rely on this feature, please open a GitHub issue + # outlining your use-case to help us decide if it should be + # part of our public interface in the future. + _strict_response_validation: bool = False, + ) -> None: + """Construct a new async AsyncCasParser client instance. + + This automatically infers the `api_key` argument from the `CAS_PARSER_API_KEY` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("CAS_PARSER_API_KEY") + if api_key is None: + raise CasParserError( + "The api_key client option must be set either by passing api_key to the client or by setting the CAS_PARSER_API_KEY environment variable" + ) + self.api_key = api_key + + self._environment = environment + + base_url_env = os.environ.get("CAS_PARSER_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `CAS_PARSER_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + + super().__init__( + version=__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + + self.cas_parser = cas_parser.AsyncCasParserResource(self) + self.cas_generator = cas_generator.AsyncCasGeneratorResource(self) + self.with_raw_response = AsyncCasParserWithRawResponse(self) + self.with_streaming_response = AsyncCasParserWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @property + @override + def auth_headers(self) -> dict[str, str]: + api_key = self.api_key + return {"x-api-key": api_key} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": f"async:{get_async_library()}", + **self._custom_headers, + } + + def copy( + self, + *, + api_key: str | None = None, + environment: Literal["production", "local"] | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + http_client: httpx.AsyncClient | None = None, + max_retries: int | NotGiven = NOT_GIVEN, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + http_client = http_client or self._client + return self.__class__( + api_key=api_key or self.api_key, + base_url=base_url or self.base_url, + environment=environment or self._environment, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, + ) + + # Alias for `copy` for nicer inline usage, e.g. + # client.with_options(timeout=10).foo.create(...) + with_options = copy + + @override + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> APIStatusError: + if response.status_code == 400: + return _exceptions.BadRequestError(err_msg, response=response, body=body) + + if response.status_code == 401: + return _exceptions.AuthenticationError(err_msg, response=response, body=body) + + if response.status_code == 403: + return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) + + if response.status_code == 404: + return _exceptions.NotFoundError(err_msg, response=response, body=body) + + if response.status_code == 409: + return _exceptions.ConflictError(err_msg, response=response, body=body) + + if response.status_code == 422: + return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) + + if response.status_code == 429: + return _exceptions.RateLimitError(err_msg, response=response, body=body) + + if response.status_code >= 500: + return _exceptions.InternalServerError(err_msg, response=response, body=body) + return APIStatusError(err_msg, response=response, body=body) + + +class CasParserWithRawResponse: + def __init__(self, client: CasParser) -> None: + self.cas_parser = cas_parser.CasParserResourceWithRawResponse(client.cas_parser) + self.cas_generator = cas_generator.CasGeneratorResourceWithRawResponse(client.cas_generator) + + +class AsyncCasParserWithRawResponse: + def __init__(self, client: AsyncCasParser) -> None: + self.cas_parser = cas_parser.AsyncCasParserResourceWithRawResponse(client.cas_parser) + self.cas_generator = cas_generator.AsyncCasGeneratorResourceWithRawResponse(client.cas_generator) + + +class CasParserWithStreamedResponse: + def __init__(self, client: CasParser) -> None: + self.cas_parser = cas_parser.CasParserResourceWithStreamingResponse(client.cas_parser) + self.cas_generator = cas_generator.CasGeneratorResourceWithStreamingResponse(client.cas_generator) + + +class AsyncCasParserWithStreamedResponse: + def __init__(self, client: AsyncCasParser) -> None: + self.cas_parser = cas_parser.AsyncCasParserResourceWithStreamingResponse(client.cas_parser) + self.cas_generator = cas_generator.AsyncCasGeneratorResourceWithStreamingResponse(client.cas_generator) + + +Client = CasParser + +AsyncClient = AsyncCasParser diff --git a/src/cas_parser/_compat.py b/src/cas_parser/_compat.py new file mode 100644 index 0000000..92d9ee6 --- /dev/null +++ b/src/cas_parser/_compat.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload +from datetime import date, datetime +from typing_extensions import Self, Literal + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import IncEx, StrBytesIntFloat + +_T = TypeVar("_T") +_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) + +# --------------- Pydantic v2 compatibility --------------- + +# Pyright incorrectly reports some of our functions as overriding a method when they don't +# pyright: reportIncompatibleMethodOverride=false + +PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +# v1 re-exports +if TYPE_CHECKING: + + def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 + ... + + def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001 + ... + + def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001 + ... + + def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001 + ... + + def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001 + ... + + def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001 + ... + + def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 + ... + +else: + if PYDANTIC_V2: + from pydantic.v1.typing import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, + ) + from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + else: + from pydantic.typing import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, + ) + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + + +# refactored config +if TYPE_CHECKING: + from pydantic import ConfigDict as ConfigDict +else: + if PYDANTIC_V2: + from pydantic import ConfigDict + else: + # TODO: provide an error message here? + ConfigDict = None + + +# renamed methods / properties +def parse_obj(model: type[_ModelT], value: object) -> _ModelT: + if PYDANTIC_V2: + return model.model_validate(value) + else: + return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + + +def field_is_required(field: FieldInfo) -> bool: + if PYDANTIC_V2: + return field.is_required() + return field.required # type: ignore + + +def field_get_default(field: FieldInfo) -> Any: + value = field.get_default() + if PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value + + +def field_outer_type(field: FieldInfo) -> Any: + if PYDANTIC_V2: + return field.annotation + return field.outer_type_ # type: ignore + + +def get_model_config(model: type[pydantic.BaseModel]) -> Any: + if PYDANTIC_V2: + return model.model_config + return model.__config__ # type: ignore + + +def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: + if PYDANTIC_V2: + return model.model_fields + return model.__fields__ # type: ignore + + +def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: + if PYDANTIC_V2: + return model.model_copy(deep=deep) + return model.copy(deep=deep) # type: ignore + + +def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: + if PYDANTIC_V2: + return model.model_dump_json(indent=indent) + return model.json(indent=indent) # type: ignore + + +def model_dump( + model: pydantic.BaseModel, + *, + exclude: IncEx | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + warnings: bool = True, + mode: Literal["json", "python"] = "python", +) -> dict[str, Any]: + if PYDANTIC_V2 or hasattr(model, "model_dump"): + return model.model_dump( + mode=mode, + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + # warnings are not supported in Pydantic v1 + warnings=warnings if PYDANTIC_V2 else True, + ) + return cast( + "dict[str, Any]", + model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + ), + ) + + +def model_parse(model: type[_ModelT], data: Any) -> _ModelT: + if PYDANTIC_V2: + return model.model_validate(data) + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + + +# generic models +if TYPE_CHECKING: + + class GenericModel(pydantic.BaseModel): ... + +else: + if PYDANTIC_V2: + # there no longer needs to be a distinction in v2 but + # we still have to create our own subclass to avoid + # inconsistent MRO ordering errors + class GenericModel(pydantic.BaseModel): ... + + else: + import pydantic.generics + + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... + + +# cached properties +if TYPE_CHECKING: + cached_property = property + + # we define a separate type (copied from typeshed) + # that represents that `cached_property` is `set`able + # at runtime, which differs from `@property`. + # + # this is a separate type as editors likely special case + # `@property` and we don't want to cause issues just to have + # more helpful internal types. + + class typed_cached_property(Generic[_T]): + func: Callable[[Any], _T] + attrname: str | None + + def __init__(self, func: Callable[[Any], _T]) -> None: ... + + @overload + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... + + @overload + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... + + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: + raise NotImplementedError() + + def __set_name__(self, owner: type[Any], name: str) -> None: ... + + # __set__ is not defined at runtime, but @cached_property is designed to be settable + def __set__(self, instance: object, value: _T) -> None: ... +else: + from functools import cached_property as cached_property + + typed_cached_property = cached_property diff --git a/src/cas_parser/_constants.py b/src/cas_parser/_constants.py new file mode 100644 index 0000000..6ddf2c7 --- /dev/null +++ b/src/cas_parser/_constants.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import httpx + +RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" +OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" + +# default timeout is 1 minute +DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) +DEFAULT_MAX_RETRIES = 2 +DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) + +INITIAL_RETRY_DELAY = 0.5 +MAX_RETRY_DELAY = 8.0 diff --git a/src/cas_parser/_exceptions.py b/src/cas_parser/_exceptions.py new file mode 100644 index 0000000..19d3605 --- /dev/null +++ b/src/cas_parser/_exceptions.py @@ -0,0 +1,108 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +__all__ = [ + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", +] + + +class CasParserError(Exception): + pass + + +class APIError(CasParserError): + message: str + request: httpx.Request + + body: object | None + """The API response body. + + If the API responded with a valid JSON structure then this property will be the + decoded result. + + If it isn't a valid JSON structure then this will be the raw response. + + If there was no response associated with this error then it will be `None`. + """ + + def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: # noqa: ARG002 + super().__init__(message) + self.request = request + self.message = message + self.body = body + + +class APIResponseValidationError(APIError): + response: httpx.Response + status_code: int + + def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None: + super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body) + self.response = response + self.status_code = response.status_code + + +class APIStatusError(APIError): + """Raised when an API response has a status code of 4xx or 5xx.""" + + response: httpx.Response + status_code: int + + def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None: + super().__init__(message, response.request, body=body) + self.response = response + self.status_code = response.status_code + + +class APIConnectionError(APIError): + def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None: + super().__init__(message, request, body=None) + + +class APITimeoutError(APIConnectionError): + def __init__(self, request: httpx.Request) -> None: + super().__init__(message="Request timed out.", request=request) + + +class BadRequestError(APIStatusError): + status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride] + + +class AuthenticationError(APIStatusError): + status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride] + + +class PermissionDeniedError(APIStatusError): + status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride] + + +class NotFoundError(APIStatusError): + status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride] + + +class ConflictError(APIStatusError): + status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride] + + +class UnprocessableEntityError(APIStatusError): + status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride] + + +class RateLimitError(APIStatusError): + status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride] + + +class InternalServerError(APIStatusError): + pass diff --git a/src/cas_parser/_files.py b/src/cas_parser/_files.py new file mode 100644 index 0000000..cc14c14 --- /dev/null +++ b/src/cas_parser/_files.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import io +import os +import pathlib +from typing import overload +from typing_extensions import TypeGuard + +import anyio + +from ._types import ( + FileTypes, + FileContent, + RequestFiles, + HttpxFileTypes, + Base64FileInput, + HttpxFileContent, + HttpxRequestFiles, +) +from ._utils import is_tuple_t, is_mapping_t, is_sequence_t + + +def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: + return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + + +def is_file_content(obj: object) -> TypeGuard[FileContent]: + return ( + isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + ) + + +def assert_is_file_content(obj: object, *, key: str | None = None) -> None: + if not is_file_content(obj): + prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" + raise RuntimeError( + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + ) from None + + +@overload +def to_httpx_files(files: None) -> None: ... + + +@overload +def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... + + +def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: _transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, _transform_file(file)) for key, file in files] + else: + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +def _transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = pathlib.Path(file) + return (path.name, path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +def read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return pathlib.Path(file).read_bytes() + return file + + +@overload +async def async_to_httpx_files(files: None) -> None: ... + + +@overload +async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... + + +async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: await _async_transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, await _async_transform_file(file)) for key, file in files] + else: + raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = anyio.Path(file) + return (path.name, await path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], await async_read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +async def async_read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return await anyio.Path(file).read_bytes() + + return file diff --git a/src/cas_parser/_models.py b/src/cas_parser/_models.py new file mode 100644 index 0000000..b8387ce --- /dev/null +++ b/src/cas_parser/_models.py @@ -0,0 +1,829 @@ +from __future__ import annotations + +import os +import inspect +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from datetime import date, datetime +from typing_extensions import ( + List, + Unpack, + Literal, + ClassVar, + Protocol, + Required, + ParamSpec, + TypedDict, + TypeGuard, + final, + override, + runtime_checkable, +) + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import ( + Body, + IncEx, + Query, + ModelT, + Headers, + Timeout, + NotGiven, + AnyMapping, + HttpxRequestFiles, +) +from ._utils import ( + PropertyInfo, + is_list, + is_given, + json_safe, + lru_cache, + is_mapping, + parse_date, + coerce_boolean, + parse_datetime, + strip_not_given, + extract_type_arg, + is_annotated_type, + is_type_alias_type, + strip_annotated_type, +) +from ._compat import ( + PYDANTIC_V2, + ConfigDict, + GenericModel as BaseGenericModel, + get_args, + is_union, + parse_obj, + get_origin, + is_literal_type, + get_model_config, + get_model_fields, + field_get_default, +) +from ._constants import RAW_RESPONSE_HEADER + +if TYPE_CHECKING: + from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema + +__all__ = ["BaseModel", "GenericModel"] + +_T = TypeVar("_T") +_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel") + +P = ParamSpec("P") + + +@runtime_checkable +class _ConfigProtocol(Protocol): + allow_population_by_field_name: bool + + +class BaseModel(pydantic.BaseModel): + if PYDANTIC_V2: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) + else: + + @property + @override + def model_fields_set(self) -> set[str]: + # a forwards-compat shim for pydantic v2 + return self.__fields_set__ # type: ignore + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + extra: Any = pydantic.Extra.allow # type: ignore + + def to_dict( + self, + *, + mode: Literal["json", "python"] = "python", + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> dict[str, object]: + """Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + mode: + If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`. + If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)` + + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value from the output. + exclude_none: Whether to exclude fields that have a value of `None` from the output. + warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2. + """ + return self.model_dump( + mode=mode, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + def to_json( + self, + *, + indent: int | None = 2, + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> str: + """Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation). + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2` + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2. + """ + return self.model_dump_json( + indent=indent, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + @override + def __str__(self) -> str: + # mypy complains about an invalid self arg + return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc] + + # Override the 'construct' method in a way that supports recursive parsing without validation. + # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. + @classmethod + @override + def construct( # pyright: ignore[reportIncompatibleMethodOverride] + __cls: Type[ModelT], + _fields_set: set[str] | None = None, + **values: object, + ) -> ModelT: + m = __cls.__new__(__cls) + fields_values: dict[str, object] = {} + + config = get_model_config(__cls) + populate_by_name = ( + config.allow_population_by_field_name + if isinstance(config, _ConfigProtocol) + else config.get("populate_by_name") + ) + + if _fields_set is None: + _fields_set = set() + + model_fields = get_model_fields(__cls) + for name, field in model_fields.items(): + key = field.alias + if key is None or (key not in values and populate_by_name): + key = name + + if key in values: + fields_values[name] = _construct_field(value=values[key], field=field, key=key) + _fields_set.add(name) + else: + fields_values[name] = field_get_default(field) + + extra_field_type = _get_extra_fields_type(__cls) + + _extra = {} + for key, value in values.items(): + if key not in model_fields: + parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value + + if PYDANTIC_V2: + _extra[key] = parsed + else: + _fields_set.add(key) + fields_values[key] = parsed + + object.__setattr__(m, "__dict__", fields_values) + + if PYDANTIC_V2: + # these properties are copied from Pydantic's `model_construct()` method + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", _extra) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) + else: + # init_private_attributes() does not exist in v2 + m._init_private_attributes() # type: ignore + + # copied from Pydantic v1's `construct()` method + object.__setattr__(m, "__fields_set__", _fields_set) + + return m + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + # because the type signatures are technically different + # although not in practice + model_construct = construct + + if not PYDANTIC_V2: + # we define aliases for some of the new pydantic v2 methods so + # that we can just document these methods without having to specify + # a specific pydantic version as some users may not know which + # pydantic version they are currently using + + @override + def model_dump( + self, + *, + mode: Literal["json", "python"] | str = "python", + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + round_trip: bool = False, + warnings: bool | Literal["none", "warn", "error"] = True, + context: dict[str, Any] | None = None, + serialize_as_any: bool = False, + ) -> dict[str, Any]: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump + + Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + Args: + mode: The mode in which `to_python` should run. + If mode is 'json', the dictionary will only contain JSON serializable types. + If mode is 'python', the dictionary may contain any Python objects. + include: A list of fields to include in the output. + exclude: A list of fields to exclude from the output. + by_alias: Whether to use the field's alias in the dictionary key if defined. + exclude_unset: Whether to exclude fields that are unset or None from the output. + exclude_defaults: Whether to exclude fields that are set to their default value from the output. + exclude_none: Whether to exclude fields that have a value of `None` from the output. + round_trip: Whether to enable serialization and deserialization round-trip support. + warnings: Whether to log warnings when invalid fields are encountered. + + Returns: + A dictionary representation of the model. + """ + if mode not in {"json", "python"}: + raise ValueError("mode must be either 'json' or 'python'") + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") + dumped = super().dict( # pyright: ignore[reportDeprecated] + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped + + @override + def model_dump_json( + self, + *, + indent: int | None = None, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + round_trip: bool = False, + warnings: bool | Literal["none", "warn", "error"] = True, + context: dict[str, Any] | None = None, + serialize_as_any: bool = False, + ) -> str: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json + + Generates a JSON representation of the model using Pydantic's `to_json` method. + + Args: + indent: Indentation to use in the JSON output. If None is passed, the output will be compact. + include: Field(s) to include in the JSON output. Can take either a string or set of strings. + exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings. + by_alias: Whether to serialize using field aliases. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + round_trip: Whether to use serialization/deserialization between JSON and class instance. + warnings: Whether to show any warnings that occurred during serialization. + + Returns: + A JSON string representation of the model. + """ + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") + return super().json( # type: ignore[reportDeprecated] + indent=indent, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + +def _construct_field(value: object, field: FieldInfo, key: str) -> object: + if value is None: + return field_get_default(field) + + if PYDANTIC_V2: + type_ = field.annotation + else: + type_ = cast(type, field.outer_type_) # type: ignore + + if type_ is None: + raise RuntimeError(f"Unexpected field type is None for {key}") + + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) + + +def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: + if not PYDANTIC_V2: + # TODO + return None + + schema = cls.__pydantic_core_schema__ + if schema["type"] == "model": + fields = schema["schema"] + if fields["type"] == "model-fields": + extras = fields.get("extras_schema") + if extras and "cls" in extras: + # mypy can't narrow the type + return extras["cls"] # type: ignore[no-any-return] + + return None + + +def is_basemodel(type_: type) -> bool: + """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" + if is_union(type_): + for variant in get_args(type_): + if is_basemodel(variant): + return True + + return False + + return is_basemodel_type(type_) + + +def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: + origin = get_origin(type_) or type_ + if not inspect.isclass(origin): + return False + return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) + + +def build( + base_model_cls: Callable[P, _BaseModelT], + *args: P.args, + **kwargs: P.kwargs, +) -> _BaseModelT: + """Construct a BaseModel class without validation. + + This is useful for cases where you need to instantiate a `BaseModel` + from an API response as this provides type-safe params which isn't supported + by helpers like `construct_type()`. + + ```py + build(MyModel, my_field_a="foo", my_field_b=123) + ``` + """ + if args: + raise TypeError( + "Received positional arguments which are not supported; Keyword arguments must be used instead", + ) + + return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) + + +def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: + """Loose coercion to the expected type with construction of nested values. + + Note: the returned value from this function is not guaranteed to match the + given type. + """ + return cast(_T, construct_type(value=value, type_=type_)) + + +def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: + """Loose coercion to the expected type with construction of nested values. + + If the given value does not match the expected type then it is returned as-is. + """ + + # store a reference to the original type we were given before we extract any inner + # types so that we can properly resolve forward references in `TypeAliasType` annotations + original_type = None + + # we allow `object` as the input type because otherwise, passing things like + # `Literal['value']` will be reported as a type error by type checkers + type_ = cast("type[object]", type_) + if is_type_alias_type(type_): + original_type = type_ # type: ignore[unreachable] + type_ = type_.__value__ # type: ignore[unreachable] + + # unwrap `Annotated[T, ...]` -> `T` + if metadata is not None and len(metadata) > 0: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] + type_ = extract_type_arg(type_, 0) + else: + meta = tuple() + + # we need to use the origin class for any types that are subscripted generics + # e.g. Dict[str, object] + origin = get_origin(type_) or type_ + args = get_args(type_) + + if is_union(origin): + try: + return validate_type(type_=cast("type[object]", original_type or type_), value=value) + except Exception: + pass + + # if the type is a discriminated union then we want to construct the right variant + # in the union, even if the data doesn't match exactly, otherwise we'd break code + # that relies on the constructed class types, e.g. + # + # class FooType: + # kind: Literal['foo'] + # value: str + # + # class BarType: + # kind: Literal['bar'] + # value: int + # + # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then + # we'd end up constructing `FooType` when it should be `BarType`. + discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta) + if discriminator and is_mapping(value): + variant_value = value.get(discriminator.field_alias_from or discriminator.field_name) + if variant_value and isinstance(variant_value, str): + variant_type = discriminator.mapping.get(variant_value) + if variant_type: + return construct_type(type_=variant_type, value=value) + + # if the data is not valid, use the first variant that doesn't fail while deserializing + for variant in args: + try: + return construct_type(value=value, type_=variant) + except Exception: + continue + + raise RuntimeError(f"Could not convert data into a valid instance of {type_}") + + if origin == dict: + if not is_mapping(value): + return value + + _, items_type = get_args(type_) # Dict[_, items_type] + return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} + + if ( + not is_literal_type(type_) + and inspect.isclass(origin) + and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)) + ): + if is_list(value): + return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value] + + if is_mapping(value): + if issubclass(type_, BaseModel): + return type_.construct(**value) # type: ignore[arg-type] + + return cast(Any, type_).construct(**value) + + if origin == list: + if not is_list(value): + return value + + inner_type = args[0] # List[inner_type] + return [construct_type(value=entry, type_=inner_type) for entry in value] + + if origin == float: + if isinstance(value, int): + coerced = float(value) + if coerced != value: + return value + return coerced + + return value + + if type_ == datetime: + try: + return parse_datetime(value) # type: ignore + except Exception: + return value + + if type_ == date: + try: + return parse_date(value) # type: ignore + except Exception: + return value + + return value + + +@runtime_checkable +class CachedDiscriminatorType(Protocol): + __discriminator__: DiscriminatorDetails + + +class DiscriminatorDetails: + field_name: str + """The name of the discriminator field in the variant class, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] + ``` + + Will result in field_name='type' + """ + + field_alias_from: str | None + """The name of the discriminator field in the API response, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] = Field(alias='type_from_api') + ``` + + Will result in field_alias_from='type_from_api' + """ + + mapping: dict[str, type] + """Mapping of discriminator value to variant type, e.g. + + {'foo': FooVariant, 'bar': BarVariant} + """ + + def __init__( + self, + *, + mapping: dict[str, type], + discriminator_field: str, + discriminator_alias: str | None, + ) -> None: + self.mapping = mapping + self.field_name = discriminator_field + self.field_alias_from = discriminator_alias + + +def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: + if isinstance(union, CachedDiscriminatorType): + return union.__discriminator__ + + discriminator_field_name: str | None = None + + for annotation in meta_annotations: + if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None: + discriminator_field_name = annotation.discriminator + break + + if not discriminator_field_name: + return None + + mapping: dict[str, type] = {} + discriminator_alias: str | None = None + + for variant in get_args(union): + variant = strip_annotated_type(variant) + if is_basemodel_type(variant): + if PYDANTIC_V2: + field = _extract_field_schema_pv2(variant, discriminator_field_name) + if not field: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field.get("serialization_alias") + + field_schema = field["schema"] + + if field_schema["type"] == "literal": + for entry in cast("LiteralSchema", field_schema)["expected"]: + if isinstance(entry, str): + mapping[entry] = variant + else: + field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + if not field_info: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field_info.alias + + if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): + for entry in get_args(annotation): + if isinstance(entry, str): + mapping[entry] = variant + + if not mapping: + return None + + details = DiscriminatorDetails( + mapping=mapping, + discriminator_field=discriminator_field_name, + discriminator_alias=discriminator_alias, + ) + cast(CachedDiscriminatorType, union).__discriminator__ = details + return details + + +def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: + schema = model.__pydantic_core_schema__ + if schema["type"] == "definitions": + schema = schema["schema"] + + if schema["type"] != "model": + return None + + schema = cast("ModelSchema", schema) + fields_schema = schema["schema"] + if fields_schema["type"] != "model-fields": + return None + + fields_schema = cast("ModelFieldsSchema", fields_schema) + field = fields_schema["fields"].get(field_name) + if not field: + return None + + return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast] + + +def validate_type(*, type_: type[_T], value: object) -> _T: + """Strict validation that the given value matches the expected type""" + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + return cast(_T, parse_obj(type_, value)) + + return cast(_T, _validate_non_model_type(type_=type_, value=value)) + + +def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: + """Add a pydantic config for the given type. + + Note: this is a no-op on Pydantic v1. + """ + setattr(typ, "__pydantic_config__", config) # noqa: B010 + + +# our use of subclassing here causes weirdness for type checkers, +# so we just pretend that we don't subclass +if TYPE_CHECKING: + GenericModel = BaseModel +else: + + class GenericModel(BaseGenericModel, BaseModel): + pass + + +if PYDANTIC_V2: + from pydantic import TypeAdapter as _TypeAdapter + + _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) + + if TYPE_CHECKING: + from pydantic import TypeAdapter + else: + TypeAdapter = _CachedTypeAdapter + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + return TypeAdapter(type_).validate_python(value) + +elif not TYPE_CHECKING: # TODO: condition is weird + + class RootModel(GenericModel, Generic[_T]): + """Used as a placeholder to easily convert runtime types to a Pydantic format + to provide validation. + + For example: + ```py + validated = RootModel[int](__root__="5").__root__ + # validated: 5 + ``` + """ + + __root__: _T + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + model = _create_pydantic_model(type_).validate(value) + return cast(_T, model.__root__) + + def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: + return RootModel[type_] # type: ignore + + +class FinalRequestOptionsInput(TypedDict, total=False): + method: Required[str] + url: Required[str] + params: Query + headers: Headers + max_retries: int + timeout: float | Timeout | None + files: HttpxRequestFiles | None + idempotency_key: str + json_data: Body + extra_json: AnyMapping + follow_redirects: bool + + +@final +class FinalRequestOptions(pydantic.BaseModel): + method: str + url: str + params: Query = {} + headers: Union[Headers, NotGiven] = NotGiven() + max_retries: Union[int, NotGiven] = NotGiven() + timeout: Union[float, Timeout, None, NotGiven] = NotGiven() + files: Union[HttpxRequestFiles, None] = None + idempotency_key: Union[str, None] = None + post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() + follow_redirects: Union[bool, None] = None + + # It should be noted that we cannot use `json` here as that would override + # a BaseModel method in an incompatible fashion. + json_data: Union[Body, None] = None + extra_json: Union[AnyMapping, None] = None + + if PYDANTIC_V2: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) + else: + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + arbitrary_types_allowed: bool = True + + def get_max_retries(self, max_retries: int) -> int: + if isinstance(self.max_retries, NotGiven): + return max_retries + return self.max_retries + + def _strip_raw_response_header(self) -> None: + if not is_given(self.headers): + return + + if self.headers.get(RAW_RESPONSE_HEADER): + self.headers = {**self.headers} + self.headers.pop(RAW_RESPONSE_HEADER) + + # override the `construct` method so that we can run custom transformations. + # this is necessary as we don't want to do any actual runtime type checking + # (which means we can't use validators) but we do want to ensure that `NotGiven` + # values are not present + # + # type ignore required because we're adding explicit types to `**values` + @classmethod + def construct( # type: ignore + cls, + _fields_set: set[str] | None = None, + **values: Unpack[FinalRequestOptionsInput], + ) -> FinalRequestOptions: + kwargs: dict[str, Any] = { + # we unconditionally call `strip_not_given` on any value + # as it will just ignore any non-mapping types + key: strip_not_given(value) + for key, value in values.items() + } + if PYDANTIC_V2: + return super().model_construct(_fields_set, **kwargs) + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + model_construct = construct diff --git a/src/cas_parser/_qs.py b/src/cas_parser/_qs.py new file mode 100644 index 0000000..274320c --- /dev/null +++ b/src/cas_parser/_qs.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from typing import Any, List, Tuple, Union, Mapping, TypeVar +from urllib.parse import parse_qs, urlencode +from typing_extensions import Literal, get_args + +from ._types import NOT_GIVEN, NotGiven, NotGivenOr +from ._utils import flatten + +_T = TypeVar("_T") + + +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + +PrimitiveData = Union[str, int, float, bool, None] +# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] +# https://github.com/microsoft/pyright/issues/3555 +Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] +Params = Mapping[str, Data] + + +class Querystring: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + *, + array_format: ArrayFormat = "repeat", + nested_format: NestedFormat = "brackets", + ) -> None: + self.array_format = array_format + self.nested_format = nested_format + + def parse(self, query: str) -> Mapping[str, object]: + # Note: custom format syntax is not supported yet + return parse_qs(query) + + def stringify( + self, + params: Params, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> str: + return urlencode( + self.stringify_items( + params, + array_format=array_format, + nested_format=nested_format, + ) + ) + + def stringify_items( + self, + params: Params, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> list[tuple[str, str]]: + opts = Options( + qs=self, + array_format=array_format, + nested_format=nested_format, + ) + return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) + + def _stringify_item( + self, + key: str, + value: Data, + opts: Options, + ) -> list[tuple[str, str]]: + if isinstance(value, Mapping): + items: list[tuple[str, str]] = [] + nested_format = opts.nested_format + for subkey, subvalue in value.items(): + items.extend( + self._stringify_item( + # TODO: error if unknown format + f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", + subvalue, + opts, + ) + ) + return items + + if isinstance(value, (list, tuple)): + array_format = opts.array_format + if array_format == "comma": + return [ + ( + key, + ",".join(self._primitive_value_to_str(item) for item in value if item is not None), + ), + ] + elif array_format == "repeat": + items = [] + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + elif array_format == "indices": + raise NotImplementedError("The array indices format is not supported yet") + elif array_format == "brackets": + items = [] + key = key + "[]" + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + else: + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + serialised = self._primitive_value_to_str(value) + if not serialised: + return [] + return [(key, serialised)] + + def _primitive_value_to_str(self, value: PrimitiveData) -> str: + # copied from httpx + if value is True: + return "true" + elif value is False: + return "false" + elif value is None: + return "" + return str(value) + + +_qs = Querystring() +parse = _qs.parse +stringify = _qs.stringify +stringify_items = _qs.stringify_items + + +class Options: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + qs: Querystring = _qs, + *, + array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, + nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + ) -> None: + self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format + self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/src/cas_parser/_resource.py b/src/cas_parser/_resource.py new file mode 100644 index 0000000..64b8370 --- /dev/null +++ b/src/cas_parser/_resource.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import anyio + +if TYPE_CHECKING: + from ._client import CasParser, AsyncCasParser + + +class SyncAPIResource: + _client: CasParser + + def __init__(self, client: CasParser) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + def _sleep(self, seconds: float) -> None: + time.sleep(seconds) + + +class AsyncAPIResource: + _client: AsyncCasParser + + def __init__(self, client: AsyncCasParser) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + async def _sleep(self, seconds: float) -> None: + await anyio.sleep(seconds) diff --git a/src/cas_parser/_response.py b/src/cas_parser/_response.py new file mode 100644 index 0000000..a67d3f1 --- /dev/null +++ b/src/cas_parser/_response.py @@ -0,0 +1,832 @@ +from __future__ import annotations + +import os +import inspect +import logging +import datetime +import functools +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Union, + Generic, + TypeVar, + Callable, + Iterator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Awaitable, ParamSpec, override, get_origin + +import anyio +import httpx +import pydantic + +from ._types import NoneType +from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base +from ._models import BaseModel, is_basemodel +from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER +from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type +from ._exceptions import CasParserError, APIResponseValidationError + +if TYPE_CHECKING: + from ._models import FinalRequestOptions + from ._base_client import BaseClient + + +P = ParamSpec("P") +R = TypeVar("R") +_T = TypeVar("_T") +_APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]") +_AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]") + +log: logging.Logger = logging.getLogger(__name__) + + +class BaseAPIResponse(Generic[R]): + _cast_to: type[R] + _client: BaseClient[Any, Any] + _parsed_by_type: dict[type[Any], Any] + _is_sse_stream: bool + _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None + _options: FinalRequestOptions + + http_response: httpx.Response + + retries_taken: int + """The number of retries made. If no retries happened this will be `0`""" + + def __init__( + self, + *, + raw: httpx.Response, + cast_to: type[R], + client: BaseClient[Any, Any], + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + options: FinalRequestOptions, + retries_taken: int = 0, + ) -> None: + self._cast_to = cast_to + self._client = client + self._parsed_by_type = {} + self._is_sse_stream = stream + self._stream_cls = stream_cls + self._options = options + self.http_response = raw + self.retries_taken = retries_taken + + @property + def headers(self) -> httpx.Headers: + return self.http_response.headers + + @property + def http_request(self) -> httpx.Request: + """Returns the httpx Request instance associated with the current response.""" + return self.http_response.request + + @property + def status_code(self) -> int: + return self.http_response.status_code + + @property + def url(self) -> httpx.URL: + """Returns the URL for which the request was made.""" + return self.http_response.url + + @property + def method(self) -> str: + return self.http_request.method + + @property + def http_version(self) -> str: + return self.http_response.http_version + + @property + def elapsed(self) -> datetime.timedelta: + """The time taken for the complete request/response cycle to complete.""" + return self.http_response.elapsed + + @property + def is_closed(self) -> bool: + """Whether or not the response body has been closed. + + If this is False then there is response data that has not been read yet. + You must either fully consume the response body or call `.close()` + before discarding the response to prevent resource leaks. + """ + return self.http_response.is_closed + + @override + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>" + ) + + def _parse(self, *, to: type[_T] | None = None) -> R | _T: + cast_to = to if to is not None else self._cast_to + + # unwrap `TypeAlias('Name', T)` -> `T` + if is_type_alias_type(cast_to): + cast_to = cast_to.__value__ # type: ignore[unreachable] + + # unwrap `Annotated[T, ...]` -> `T` + if cast_to and is_annotated_type(cast_to): + cast_to = extract_type_arg(cast_to, 0) + + origin = get_origin(cast_to) or cast_to + + if self._is_sse_stream: + if to: + if not is_stream_class_type(to): + raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}") + + return cast( + _T, + to( + cast_to=extract_stream_chunk_type( + to, + failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]", + ), + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + if self._stream_cls: + return cast( + R, + self._stream_cls( + cast_to=extract_stream_chunk_type(self._stream_cls), + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls) + if stream_cls is None: + raise MissingStreamClassError() + + return cast( + R, + stream_cls( + cast_to=cast_to, + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + if cast_to is NoneType: + return cast(R, None) + + response = self.http_response + if cast_to == str: + return cast(R, response.text) + + if cast_to == bytes: + return cast(R, response.content) + + if cast_to == int: + return cast(R, int(response.text)) + + if cast_to == float: + return cast(R, float(response.text)) + + if cast_to == bool: + return cast(R, response.text.lower() == "true") + + if origin == APIResponse: + raise RuntimeError("Unexpected state - cast_to is `APIResponse`") + + if inspect.isclass(origin) and issubclass(origin, httpx.Response): + # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response + # and pass that class to our request functions. We cannot change the variance to be either + # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct + # the response class ourselves but that is something that should be supported directly in httpx + # as it would be easy to incorrectly construct the Response object due to the multitude of arguments. + if cast_to != httpx.Response: + raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") + return cast(R, response) + + if ( + inspect.isclass( + origin # pyright: ignore[reportUnknownArgumentType] + ) + and not issubclass(origin, BaseModel) + and issubclass(origin, pydantic.BaseModel) + ): + raise TypeError( + "Pydantic models must subclass our base model type, e.g. `from cas_parser import BaseModel`" + ) + + if ( + cast_to is not object + and not origin is list + and not origin is dict + and not origin is Union + and not issubclass(origin, BaseModel) + ): + raise RuntimeError( + f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}." + ) + + # split is required to handle cases where additional information is included + # in the response, e.g. application/json; charset=utf-8 + content_type, *_ = response.headers.get("content-type", "*").split(";") + if not content_type.endswith("json"): + if is_basemodel(cast_to): + try: + data = response.json() + except Exception as exc: + log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc) + else: + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + if self._client._strict_response_validation: + raise APIResponseValidationError( + response=response, + message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", + body=response.text, + ) + + # If the API responds with content that isn't JSON then we just return + # the (decoded) text without performing any parsing so that you can still + # handle the response however you need to. + return response.text # type: ignore + + data = response.json() + + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + +class APIResponse(BaseAPIResponse[R]): + @overload + def parse(self, *, to: type[_T]) -> _T: ... + + @overload + def parse(self) -> R: ... + + def parse(self, *, to: type[_T] | None = None) -> R | _T: + """Returns the rich python representation of this response's data. + + For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. + + You can customise the type that the response is parsed into through + the `to` argument, e.g. + + ```py + from cas_parser import BaseModel + + + class MyModel(BaseModel): + foo: str + + + obj = response.parse(to=MyModel) + print(obj.foo) + ``` + + We support parsing: + - `BaseModel` + - `dict` + - `list` + - `Union` + - `str` + - `int` + - `float` + - `httpx.Response` + """ + cache_key = to if to is not None else self._cast_to + cached = self._parsed_by_type.get(cache_key) + if cached is not None: + return cached # type: ignore[no-any-return] + + if not self._is_sse_stream: + self.read() + + parsed = self._parse(to=to) + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed_by_type[cache_key] = parsed + return parsed + + def read(self) -> bytes: + """Read and return the binary response content.""" + try: + return self.http_response.read() + except httpx.StreamConsumed as exc: + # The default error raised by httpx isn't very + # helpful in our case so we re-raise it with + # a different error message. + raise StreamAlreadyConsumed() from exc + + def text(self) -> str: + """Read and decode the response content into a string.""" + self.read() + return self.http_response.text + + def json(self) -> object: + """Read and decode the JSON response content.""" + self.read() + return self.http_response.json() + + def close(self) -> None: + """Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.http_response.close() + + def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]: + """ + A byte-iterator over the decoded response content. + + This automatically handles gzip, deflate and brotli encoded responses. + """ + for chunk in self.http_response.iter_bytes(chunk_size): + yield chunk + + def iter_text(self, chunk_size: int | None = None) -> Iterator[str]: + """A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + for chunk in self.http_response.iter_text(chunk_size): + yield chunk + + def iter_lines(self) -> Iterator[str]: + """Like `iter_text()` but will only yield chunks for each line""" + for chunk in self.http_response.iter_lines(): + yield chunk + + +class AsyncAPIResponse(BaseAPIResponse[R]): + @overload + async def parse(self, *, to: type[_T]) -> _T: ... + + @overload + async def parse(self) -> R: ... + + async def parse(self, *, to: type[_T] | None = None) -> R | _T: + """Returns the rich python representation of this response's data. + + For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. + + You can customise the type that the response is parsed into through + the `to` argument, e.g. + + ```py + from cas_parser import BaseModel + + + class MyModel(BaseModel): + foo: str + + + obj = response.parse(to=MyModel) + print(obj.foo) + ``` + + We support parsing: + - `BaseModel` + - `dict` + - `list` + - `Union` + - `str` + - `httpx.Response` + """ + cache_key = to if to is not None else self._cast_to + cached = self._parsed_by_type.get(cache_key) + if cached is not None: + return cached # type: ignore[no-any-return] + + if not self._is_sse_stream: + await self.read() + + parsed = self._parse(to=to) + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed_by_type[cache_key] = parsed + return parsed + + async def read(self) -> bytes: + """Read and return the binary response content.""" + try: + return await self.http_response.aread() + except httpx.StreamConsumed as exc: + # the default error raised by httpx isn't very + # helpful in our case so we re-raise it with + # a different error message + raise StreamAlreadyConsumed() from exc + + async def text(self) -> str: + """Read and decode the response content into a string.""" + await self.read() + return self.http_response.text + + async def json(self) -> object: + """Read and decode the JSON response content.""" + await self.read() + return self.http_response.json() + + async def close(self) -> None: + """Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.http_response.aclose() + + async def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]: + """ + A byte-iterator over the decoded response content. + + This automatically handles gzip, deflate and brotli encoded responses. + """ + async for chunk in self.http_response.aiter_bytes(chunk_size): + yield chunk + + async def iter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]: + """A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + async for chunk in self.http_response.aiter_text(chunk_size): + yield chunk + + async def iter_lines(self) -> AsyncIterator[str]: + """Like `iter_text()` but will only yield chunks for each line""" + async for chunk in self.http_response.aiter_lines(): + yield chunk + + +class BinaryAPIResponse(APIResponse[bytes]): + """Subclass of APIResponse providing helpers for dealing with binary data. + + Note: If you want to stream the response data instead of eagerly reading it + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + + def write_to_file( + self, + file: str | os.PathLike[str], + ) -> None: + """Write the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + + Note: if you want to stream the data to the file instead of writing + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + with open(file, mode="wb") as f: + for data in self.iter_bytes(): + f.write(data) + + +class AsyncBinaryAPIResponse(AsyncAPIResponse[bytes]): + """Subclass of APIResponse providing helpers for dealing with binary data. + + Note: If you want to stream the response data instead of eagerly reading it + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + + async def write_to_file( + self, + file: str | os.PathLike[str], + ) -> None: + """Write the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + + Note: if you want to stream the data to the file instead of writing + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + path = anyio.Path(file) + async with await path.open(mode="wb") as f: + async for data in self.iter_bytes(): + await f.write(data) + + +class StreamedBinaryAPIResponse(APIResponse[bytes]): + def stream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + """Streams the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + """ + with open(file, mode="wb") as f: + for data in self.iter_bytes(chunk_size): + f.write(data) + + +class AsyncStreamedBinaryAPIResponse(AsyncAPIResponse[bytes]): + async def stream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + """Streams the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + """ + path = anyio.Path(file) + async with await path.open(mode="wb") as f: + async for data in self.iter_bytes(chunk_size): + await f.write(data) + + +class MissingStreamClassError(TypeError): + def __init__(self) -> None: + super().__init__( + "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `cas_parser._streaming` for reference", + ) + + +class StreamAlreadyConsumed(CasParserError): + """ + Attempted to read or stream content, but the content has already + been streamed. + + This can happen if you use a method like `.iter_lines()` and then attempt + to read th entire response body afterwards, e.g. + + ```py + response = await client.post(...) + async for line in response.iter_lines(): + ... # do something with `line` + + content = await response.read() + # ^ error + ``` + + If you want this behaviour you'll need to either manually accumulate the response + content or call `await response.read()` before iterating over the stream. + """ + + def __init__(self) -> None: + message = ( + "Attempted to read or stream some content, but the content has " + "already been streamed. " + "This could be due to attempting to stream the response " + "content more than once." + "\n\n" + "You can fix this by manually accumulating the response content while streaming " + "or by calling `.read()` before starting to stream." + ) + super().__init__(message) + + +class ResponseContextManager(Generic[_APIResponseT]): + """Context manager for ensuring that a request is not made + until it is entered and that the response will always be closed + when the context manager exits + """ + + def __init__(self, request_func: Callable[[], _APIResponseT]) -> None: + self._request_func = request_func + self.__response: _APIResponseT | None = None + + def __enter__(self) -> _APIResponseT: + self.__response = self._request_func() + return self.__response + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__response is not None: + self.__response.close() + + +class AsyncResponseContextManager(Generic[_AsyncAPIResponseT]): + """Context manager for ensuring that a request is not made + until it is entered and that the response will always be closed + when the context manager exits + """ + + def __init__(self, api_request: Awaitable[_AsyncAPIResponseT]) -> None: + self._api_request = api_request + self.__response: _AsyncAPIResponseT | None = None + + async def __aenter__(self) -> _AsyncAPIResponseT: + self.__response = await self._api_request + return self.__response + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__response is not None: + await self.__response.close() + + +def to_streamed_response_wrapper(func: Callable[P, R]) -> Callable[P, ResponseContextManager[APIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support streaming and returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[APIResponse[R]]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + + kwargs["extra_headers"] = extra_headers + + make_request = functools.partial(func, *args, **kwargs) + + return ResponseContextManager(cast(Callable[[], APIResponse[R]], make_request)) + + return wrapped + + +def async_to_streamed_response_wrapper( + func: Callable[P, Awaitable[R]], +) -> Callable[P, AsyncResponseContextManager[AsyncAPIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support streaming and returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[AsyncAPIResponse[R]]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + + kwargs["extra_headers"] = extra_headers + + make_request = func(*args, **kwargs) + + return AsyncResponseContextManager(cast(Awaitable[AsyncAPIResponse[R]], make_request)) + + return wrapped + + +def to_custom_streamed_response_wrapper( + func: Callable[P, object], + response_cls: type[_APIResponseT], +) -> Callable[P, ResponseContextManager[_APIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support streaming and returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[_APIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + make_request = functools.partial(func, *args, **kwargs) + + return ResponseContextManager(cast(Callable[[], _APIResponseT], make_request)) + + return wrapped + + +def async_to_custom_streamed_response_wrapper( + func: Callable[P, Awaitable[object]], + response_cls: type[_AsyncAPIResponseT], +) -> Callable[P, AsyncResponseContextManager[_AsyncAPIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support streaming and returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[_AsyncAPIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + make_request = func(*args, **kwargs) + + return AsyncResponseContextManager(cast(Awaitable[_AsyncAPIResponseT], make_request)) + + return wrapped + + +def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + + kwargs["extra_headers"] = extra_headers + + return cast(APIResponse[R], func(*args, **kwargs)) + + return wrapped + + +def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[AsyncAPIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + + kwargs["extra_headers"] = extra_headers + + return cast(AsyncAPIResponse[R], await func(*args, **kwargs)) + + return wrapped + + +def to_custom_raw_response_wrapper( + func: Callable[P, object], + response_cls: type[_APIResponseT], +) -> Callable[P, _APIResponseT]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> _APIResponseT: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + return cast(_APIResponseT, func(*args, **kwargs)) + + return wrapped + + +def async_to_custom_raw_response_wrapper( + func: Callable[P, Awaitable[object]], + response_cls: type[_AsyncAPIResponseT], +) -> Callable[P, Awaitable[_AsyncAPIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> Awaitable[_AsyncAPIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + return cast(Awaitable[_AsyncAPIResponseT], func(*args, **kwargs)) + + return wrapped + + +def extract_response_type(typ: type[BaseAPIResponse[Any]]) -> type: + """Given a type like `APIResponse[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyResponse(APIResponse[bytes]): + ... + + extract_response_type(MyResponse) -> bytes + ``` + """ + return extract_type_var_from_base( + typ, + generic_bases=cast("tuple[type, ...]", (BaseAPIResponse, APIResponse, AsyncAPIResponse)), + index=0, + ) diff --git a/src/cas_parser/_streaming.py b/src/cas_parser/_streaming.py new file mode 100644 index 0000000..9c9eb3e --- /dev/null +++ b/src/cas_parser/_streaming.py @@ -0,0 +1,333 @@ +# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py +from __future__ import annotations + +import json +import inspect +from types import TracebackType +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable + +import httpx + +from ._utils import extract_type_var_from_base + +if TYPE_CHECKING: + from ._client import CasParser, AsyncCasParser + + +_T = TypeVar("_T") + + +class Stream(Generic[_T]): + """Provides the core interface to iterate over a synchronous stream response.""" + + response: httpx.Response + + _decoder: SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: CasParser, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + def __next__(self) -> _T: + return self._iterator.__next__() + + def __iter__(self) -> Iterator[_T]: + for item in self._iterator: + yield item + + def _iter_events(self) -> Iterator[ServerSentEvent]: + yield from self._decoder.iter_bytes(self.response.iter_bytes()) + + def __stream__(self) -> Iterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + # Ensure the entire stream is consumed + for _sse in iterator: + ... + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.response.close() + + +class AsyncStream(Generic[_T]): + """Provides the core interface to iterate over an asynchronous stream response.""" + + response: httpx.Response + + _decoder: SSEDecoder | SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: AsyncCasParser, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + async def __anext__(self) -> _T: + return await self._iterator.__anext__() + + async def __aiter__(self) -> AsyncIterator[_T]: + async for item in self._iterator: + yield item + + async def _iter_events(self) -> AsyncIterator[ServerSentEvent]: + async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()): + yield sse + + async def __stream__(self) -> AsyncIterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + + # Ensure the entire stream is consumed + async for _sse in iterator: + ... + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.response.aclose() + + +class ServerSentEvent: + def __init__( + self, + *, + event: str | None = None, + data: str | None = None, + id: str | None = None, + retry: int | None = None, + ) -> None: + if data is None: + data = "" + + self._id = id + self._data = data + self._event = event or None + self._retry = retry + + @property + def event(self) -> str | None: + return self._event + + @property + def id(self) -> str | None: + return self._id + + @property + def retry(self) -> int | None: + return self._retry + + @property + def data(self) -> str: + return self._data + + def json(self) -> Any: + return json.loads(self.data) + + @override + def __repr__(self) -> str: + return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" + + +class SSEDecoder: + _data: list[str] + _event: str | None + _retry: int | None + _last_event_id: str | None + + def __init__(self) -> None: + self._event = None + self._data = [] + self._last_event_id = None + self._retry = None + + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + for chunk in self._iter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + async for chunk in self._aiter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + async for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + def decode(self, line: str) -> ServerSentEvent | None: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = None + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None + + +@runtime_checkable +class SSEBytesDecoder(Protocol): + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an async iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + +def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]: + """TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`""" + origin = get_origin(typ) or typ + return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream)) + + +def extract_stream_chunk_type( + stream_cls: type, + *, + failure_message: str | None = None, +) -> type: + """Given a type like `Stream[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyStream(Stream[bytes]): + ... + + extract_stream_chunk_type(MyStream) -> bytes + ``` + """ + from ._base_client import Stream, AsyncStream + + return extract_type_var_from_base( + stream_cls, + index=0, + generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)), + failure_message=failure_message, + ) diff --git a/src/cas_parser/_types.py b/src/cas_parser/_types.py new file mode 100644 index 0000000..87f9f3d --- /dev/null +++ b/src/cas_parser/_types.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + Dict, + List, + Type, + Tuple, + Union, + Mapping, + TypeVar, + Callable, + Optional, + Sequence, +) +from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable + +import httpx +import pydantic +from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport + +if TYPE_CHECKING: + from ._models import BaseModel + from ._response import APIResponse, AsyncAPIResponse + +Transport = BaseTransport +AsyncTransport = AsyncBaseTransport +Query = Mapping[str, object] +Body = object +AnyMapping = Mapping[str, object] +ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) +_T = TypeVar("_T") + + +# Approximates httpx internal ProxiesTypes and RequestFiles types +# while adding support for `PathLike` instances +ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] +ProxiesTypes = Union[str, Proxy, ProxiesDict] +if TYPE_CHECKING: + Base64FileInput = Union[IO[bytes], PathLike[str]] + FileContent = Union[IO[bytes], bytes, PathLike[str]] +else: + Base64FileInput = Union[IO[bytes], PathLike] + FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. +FileTypes = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] + +# duplicate of the above but without our custom file support +HttpxFileContent = Union[IO[bytes], bytes] +HttpxFileTypes = Union[ + # file (or bytes) + HttpxFileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], HttpxFileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], HttpxFileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]], +] +HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]] + +# Workaround to support (cast_to: Type[ResponseT]) -> ResponseT +# where ResponseT includes `None`. In order to support directly +# passing `None`, overloads would have to be defined for every +# method that uses `ResponseT` which would lead to an unacceptable +# amount of code duplication and make it unreadable. See _base_client.py +# for example usage. +# +# This unfortunately means that you will either have +# to import this type and pass it explicitly: +# +# from cas_parser import NoneType +# client.get('/foo', cast_to=NoneType) +# +# or build it yourself: +# +# client.get('/foo', cast_to=type(None)) +if TYPE_CHECKING: + NoneType: Type[None] +else: + NoneType = type(None) + + +class RequestOptions(TypedDict, total=False): + headers: Headers + max_retries: int + timeout: float | Timeout | None + params: Query + extra_json: AnyMapping + idempotency_key: str + follow_redirects: bool + + +# Sentinel class used until PEP 0661 is accepted +class NotGiven: + """ + A sentinel singleton class used to distinguish omitted keyword arguments + from those passed in with the value None (which may have different behavior). + + For example: + + ```py + def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... + + + get(timeout=1) # 1s timeout + get(timeout=None) # No timeout + get() # Default timeout behavior, which may not be statically known at the method definition. + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + @override + def __repr__(self) -> str: + return "NOT_GIVEN" + + +NotGivenOr = Union[_T, NotGiven] +NOT_GIVEN = NotGiven() + + +class Omit: + """In certain situations you need to be able to represent a case where a default value has + to be explicitly removed and `None` is not an appropriate substitute, for example: + + ```py + # as the default `Content-Type` header is `application/json` that will be sent + client.post("/upload/files", files={"file": b"my raw file content"}) + + # you can't explicitly override the header as it has to be dynamically generated + # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' + client.post(..., headers={"Content-Type": "multipart/form-data"}) + + # instead you can remove the default `application/json` header by passing Omit + client.post(..., headers={"Content-Type": Omit()}) + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + +@runtime_checkable +class ModelBuilderProtocol(Protocol): + @classmethod + def build( + cls: type[_T], + *, + response: Response, + data: object, + ) -> _T: ... + + +Headers = Mapping[str, Union[str, Omit]] + + +class HeadersLikeProtocol(Protocol): + def get(self, __key: str) -> str | None: ... + + +HeadersLike = Union[Headers, HeadersLikeProtocol] + +ResponseT = TypeVar( + "ResponseT", + bound=Union[ + object, + str, + None, + "BaseModel", + List[Any], + Dict[str, Any], + Response, + ModelBuilderProtocol, + "APIResponse[Any]", + "AsyncAPIResponse[Any]", + ], +) + +StrBytesIntFloat = Union[str, bytes, int, float] + +# Note: copied from Pydantic +# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 +IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] + +PostParser = Callable[[Any], Any] + + +@runtime_checkable +class InheritsGeneric(Protocol): + """Represents a type that has inherited from `Generic` + + The `__orig_bases__` property can be used to determine the resolved + type variable for a given base class. + """ + + __orig_bases__: tuple[_GenericAlias] + + +class _GenericAlias(Protocol): + __origin__: type[object] + + +class HttpxSendArgs(TypedDict, total=False): + auth: httpx.Auth + follow_redirects: bool diff --git a/src/cas_parser/_utils/__init__.py b/src/cas_parser/_utils/__init__.py new file mode 100644 index 0000000..d4fda26 --- /dev/null +++ b/src/cas_parser/_utils/__init__.py @@ -0,0 +1,57 @@ +from ._sync import asyncify as asyncify +from ._proxy import LazyProxy as LazyProxy +from ._utils import ( + flatten as flatten, + is_dict as is_dict, + is_list as is_list, + is_given as is_given, + is_tuple as is_tuple, + json_safe as json_safe, + lru_cache as lru_cache, + is_mapping as is_mapping, + is_tuple_t as is_tuple_t, + parse_date as parse_date, + is_iterable as is_iterable, + is_sequence as is_sequence, + coerce_float as coerce_float, + is_mapping_t as is_mapping_t, + removeprefix as removeprefix, + removesuffix as removesuffix, + extract_files as extract_files, + is_sequence_t as is_sequence_t, + required_args as required_args, + coerce_boolean as coerce_boolean, + coerce_integer as coerce_integer, + file_from_path as file_from_path, + parse_datetime as parse_datetime, + strip_not_given as strip_not_given, + deepcopy_minimal as deepcopy_minimal, + get_async_library as get_async_library, + maybe_coerce_float as maybe_coerce_float, + get_required_header as get_required_header, + maybe_coerce_boolean as maybe_coerce_boolean, + maybe_coerce_integer as maybe_coerce_integer, +) +from ._typing import ( + is_list_type as is_list_type, + is_union_type as is_union_type, + extract_type_arg as extract_type_arg, + is_iterable_type as is_iterable_type, + is_required_type as is_required_type, + is_annotated_type as is_annotated_type, + is_type_alias_type as is_type_alias_type, + strip_annotated_type as strip_annotated_type, + extract_type_var_from_base as extract_type_var_from_base, +) +from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator +from ._transform import ( + PropertyInfo as PropertyInfo, + transform as transform, + async_transform as async_transform, + maybe_transform as maybe_transform, + async_maybe_transform as async_maybe_transform, +) +from ._reflection import ( + function_has_argument as function_has_argument, + assert_signatures_in_sync as assert_signatures_in_sync, +) diff --git a/src/cas_parser/_utils/_logs.py b/src/cas_parser/_utils/_logs.py new file mode 100644 index 0000000..b547588 --- /dev/null +++ b/src/cas_parser/_utils/_logs.py @@ -0,0 +1,25 @@ +import os +import logging + +logger: logging.Logger = logging.getLogger("cas_parser") +httpx_logger: logging.Logger = logging.getLogger("httpx") + + +def _basic_config() -> None: + # e.g. [2023-10-05 14:12:26 - cas_parser._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" + logging.basicConfig( + format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + +def setup_logging() -> None: + env = os.environ.get("CAS_PARSER_LOG") + if env == "debug": + _basic_config() + logger.setLevel(logging.DEBUG) + httpx_logger.setLevel(logging.DEBUG) + elif env == "info": + _basic_config() + logger.setLevel(logging.INFO) + httpx_logger.setLevel(logging.INFO) diff --git a/src/cas_parser/_utils/_proxy.py b/src/cas_parser/_utils/_proxy.py new file mode 100644 index 0000000..0f239a3 --- /dev/null +++ b/src/cas_parser/_utils/_proxy.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Generic, TypeVar, Iterable, cast +from typing_extensions import override + +T = TypeVar("T") + + +class LazyProxy(Generic[T], ABC): + """Implements data methods to pretend that an instance is another instance. + + This includes forwarding attribute access and other methods. + """ + + # Note: we have to special case proxies that themselves return proxies + # to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz` + + def __getattr__(self, attr: str) -> object: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied # pyright: ignore + return getattr(proxied, attr) + + @override + def __repr__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return repr(self.__get_proxied__()) + + @override + def __str__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return str(proxied) + + @override + def __dir__(self) -> Iterable[str]: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return [] + return proxied.__dir__() + + @property # type: ignore + @override + def __class__(self) -> type: # pyright: ignore + try: + proxied = self.__get_proxied__() + except Exception: + return type(self) + if issubclass(type(proxied), LazyProxy): + return type(proxied) + return proxied.__class__ + + def __get_proxied__(self) -> T: + return self.__load__() + + def __as_proxied__(self) -> T: + """Helper method that returns the current proxy, typed as the loaded object""" + return cast(T, self) + + @abstractmethod + def __load__(self) -> T: ... diff --git a/src/cas_parser/_utils/_reflection.py b/src/cas_parser/_utils/_reflection.py new file mode 100644 index 0000000..89aa712 --- /dev/null +++ b/src/cas_parser/_utils/_reflection.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import inspect +from typing import Any, Callable + + +def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool: + """Returns whether or not the given function has a specific parameter""" + sig = inspect.signature(func) + return arg_name in sig.parameters + + +def assert_signatures_in_sync( + source_func: Callable[..., Any], + check_func: Callable[..., Any], + *, + exclude_params: set[str] = set(), +) -> None: + """Ensure that the signature of the second function matches the first.""" + + check_sig = inspect.signature(check_func) + source_sig = inspect.signature(source_func) + + errors: list[str] = [] + + for name, source_param in source_sig.parameters.items(): + if name in exclude_params: + continue + + custom_param = check_sig.parameters.get(name) + if not custom_param: + errors.append(f"the `{name}` param is missing") + continue + + if custom_param.annotation != source_param.annotation: + errors.append( + f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}" + ) + continue + + if errors: + raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors)) diff --git a/src/cas_parser/_utils/_resources_proxy.py b/src/cas_parser/_utils/_resources_proxy.py new file mode 100644 index 0000000..bb89d3e --- /dev/null +++ b/src/cas_parser/_utils/_resources_proxy.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import Any +from typing_extensions import override + +from ._proxy import LazyProxy + + +class ResourcesProxy(LazyProxy[Any]): + """A proxy for the `cas_parser.resources` module. + + This is used so that we can lazily import `cas_parser.resources` only when + needed *and* so that users can just import `cas_parser` and reference `cas_parser.resources` + """ + + @override + def __load__(self) -> Any: + import importlib + + mod = importlib.import_module("cas_parser.resources") + return mod + + +resources = ResourcesProxy().__as_proxied__() diff --git a/src/cas_parser/_utils/_streams.py b/src/cas_parser/_utils/_streams.py new file mode 100644 index 0000000..f4a0208 --- /dev/null +++ b/src/cas_parser/_utils/_streams.py @@ -0,0 +1,12 @@ +from typing import Any +from typing_extensions import Iterator, AsyncIterator + + +def consume_sync_iterator(iterator: Iterator[Any]) -> None: + for _ in iterator: + ... + + +async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None: + async for _ in iterator: + ... diff --git a/src/cas_parser/_utils/_sync.py b/src/cas_parser/_utils/_sync.py new file mode 100644 index 0000000..ad7ec71 --- /dev/null +++ b/src/cas_parser/_utils/_sync.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import sys +import asyncio +import functools +import contextvars +from typing import Any, TypeVar, Callable, Awaitable +from typing_extensions import ParamSpec + +import anyio +import sniffio +import anyio.to_thread + +T_Retval = TypeVar("T_Retval") +T_ParamSpec = ParamSpec("T_ParamSpec") + + +if sys.version_info >= (3, 9): + _asyncio_to_thread = asyncio.to_thread +else: + # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread + # for Python 3.8 support + async def _asyncio_to_thread( + func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs + ) -> Any: + """Asynchronously run function *func* in a separate thread. + + Any *args and **kwargs supplied for this function are directly passed + to *func*. Also, the current :class:`contextvars.Context` is propagated, + allowing context variables from the main thread to be accessed in the + separate thread. + + Returns a coroutine that can be awaited to get the eventual result of *func*. + """ + loop = asyncio.events.get_running_loop() + ctx = contextvars.copy_context() + func_call = functools.partial(ctx.run, func, *args, **kwargs) + return await loop.run_in_executor(None, func_call) + + +async def to_thread( + func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs +) -> T_Retval: + if sniffio.current_async_library() == "asyncio": + return await _asyncio_to_thread(func, *args, **kwargs) + + return await anyio.to_thread.run_sync( + functools.partial(func, *args, **kwargs), + ) + + +# inspired by `asyncer`, https://github.com/tiangolo/asyncer +def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: + """ + Take a blocking function and create an async one that receives the same + positional and keyword arguments. For python version 3.9 and above, it uses + asyncio.to_thread to run the function in a separate thread. For python version + 3.8, it uses locally defined copy of the asyncio.to_thread function which was + introduced in python 3.9. + + Usage: + + ```python + def blocking_func(arg1, arg2, kwarg1=None): + # blocking code + return result + + + result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) + ``` + + ## Arguments + + `function`: a blocking regular callable (e.g. a function) + + ## Return + + An async function that takes the same positional and keyword arguments as the + original one, that when called runs the same original function in a thread worker + and returns the result. + """ + + async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: + return await to_thread(function, *args, **kwargs) + + return wrapper diff --git a/src/cas_parser/_utils/_transform.py b/src/cas_parser/_utils/_transform.py new file mode 100644 index 0000000..b0cc20a --- /dev/null +++ b/src/cas_parser/_utils/_transform.py @@ -0,0 +1,447 @@ +from __future__ import annotations + +import io +import base64 +import pathlib +from typing import Any, Mapping, TypeVar, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints + +import anyio +import pydantic + +from ._utils import ( + is_list, + is_given, + lru_cache, + is_mapping, + is_iterable, +) +from .._files import is_base64_file_input +from ._typing import ( + is_list_type, + is_union_type, + extract_type_arg, + is_iterable_type, + is_required_type, + is_annotated_type, + strip_annotated_type, +) +from .._compat import get_origin, model_dump, is_typeddict + +_T = TypeVar("_T") + + +# TODO: support for drilling globals() and locals() +# TODO: ensure works correctly with forward references in all cases + + +PropertyFormat = Literal["iso8601", "base64", "custom"] + + +class PropertyInfo: + """Metadata class to be used in Annotated types to provide information about a given type. + + For example: + + class MyParams(TypedDict): + account_holder_name: Annotated[str, PropertyInfo(alias='accountHolderName')] + + This means that {'account_holder_name': 'Robert'} will be transformed to {'accountHolderName': 'Robert'} before being sent to the API. + """ + + alias: str | None + format: PropertyFormat | None + format_template: str | None + discriminator: str | None + + def __init__( + self, + *, + alias: str | None = None, + format: PropertyFormat | None = None, + format_template: str | None = None, + discriminator: str | None = None, + ) -> None: + self.alias = alias + self.format = format + self.format_template = format_template + self.discriminator = discriminator + + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')" + + +def maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `transform()` that allows `None` to be passed. + + See `transform()` for more details. + """ + if data is None: + return None + return transform(data, expected_type) + + +# Wrapper over _transform_recursive providing fake types +def transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = _transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +@lru_cache(maxsize=8096) +def _get_annotated_type(type_: type) -> type | None: + """If the given type is an `Annotated` type then it is returned, if not `None` is returned. + + This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]` + """ + if is_required_type(type_): + # Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]` + type_ = get_args(type_)[0] + + if is_annotated_type(type_): + return type_ + + return None + + +def _maybe_transform_key(key: str, type_: type) -> str: + """Transform the given `data` based on the annotations provided in `type_`. + + Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata. + """ + annotated_type = _get_annotated_type(type_) + if annotated_type is None: + # no `Annotated` definition for this type, no transformation needed + return key + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.alias is not None: + return annotation.alias + + return key + + +def _no_transform_needed(annotation: type) -> bool: + return annotation == float or annotation == int + + +def _transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type + if is_typeddict(stripped_type) and is_mapping(data): + return _transform_typeddict(data, stripped_type) + + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + + inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + + return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = _transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True, mode="json") + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return _format_data(data, annotation.format, annotation.format_template) + + return data + + +def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = data.read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +def _transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + if not is_given(value): + # we don't need to include `NotGiven` values here as they'll + # be stripped out before the request is sent anyway + continue + + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_) + return result + + +async def async_maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `async_transform()` that allows `None` to be passed. + + See `async_transform()` for more details. + """ + if data is None: + return None + return await async_transform(data, expected_type) + + +async def async_transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +async def _async_transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type + if is_typeddict(stripped_type) and is_mapping(data): + return await _async_transform_typeddict(data, stripped_type) + + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + + inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + + return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True, mode="json") + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return await _async_format_data(data, annotation.format, annotation.format_template) + + return data + + +async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = await anyio.Path(data).read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +async def _async_transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + if not is_given(value): + # we don't need to include `NotGiven` values here as they'll + # be stripped out before the request is sent anyway + continue + + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) + return result + + +@lru_cache(maxsize=8096) +def get_type_hints( + obj: Any, + globalns: dict[str, Any] | None = None, + localns: Mapping[str, Any] | None = None, + include_extras: bool = False, +) -> dict[str, Any]: + return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) diff --git a/src/cas_parser/_utils/_typing.py b/src/cas_parser/_utils/_typing.py new file mode 100644 index 0000000..1bac954 --- /dev/null +++ b/src/cas_parser/_utils/_typing.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +import sys +import typing +import typing_extensions +from typing import Any, TypeVar, Iterable, cast +from collections import abc as _c_abc +from typing_extensions import ( + TypeIs, + Required, + Annotated, + get_args, + get_origin, +) + +from ._utils import lru_cache +from .._types import InheritsGeneric +from .._compat import is_union as _is_union + + +def is_annotated_type(typ: type) -> bool: + return get_origin(typ) == Annotated + + +def is_list_type(typ: type) -> bool: + return (get_origin(typ) or typ) == list + + +def is_iterable_type(typ: type) -> bool: + """If the given type is `typing.Iterable[T]`""" + origin = get_origin(typ) or typ + return origin == Iterable or origin == _c_abc.Iterable + + +def is_union_type(typ: type) -> bool: + return _is_union(get_origin(typ)) + + +def is_required_type(typ: type) -> bool: + return get_origin(typ) == Required + + +def is_typevar(typ: type) -> bool: + # type ignore is required because type checkers + # think this expression will always return False + return type(typ) == TypeVar # type: ignore + + +_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) +if sys.version_info >= (3, 12): + _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) + + +def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: + """Return whether the provided argument is an instance of `TypeAliasType`. + + ```python + type Int = int + is_type_alias_type(Int) + # > True + Str = TypeAliasType("Str", str) + is_type_alias_type(Str) + # > True + ``` + """ + return isinstance(tp, _TYPE_ALIAS_TYPES) + + +# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] +@lru_cache(maxsize=8096) +def strip_annotated_type(typ: type) -> type: + if is_required_type(typ) or is_annotated_type(typ): + return strip_annotated_type(cast(type, get_args(typ)[0])) + + return typ + + +def extract_type_arg(typ: type, index: int) -> type: + args = get_args(typ) + try: + return cast(type, args[index]) + except IndexError as err: + raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err + + +def extract_type_var_from_base( + typ: type, + *, + generic_bases: tuple[type, ...], + index: int, + failure_message: str | None = None, +) -> type: + """Given a type like `Foo[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyResponse(Foo[bytes]): + ... + + extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes + ``` + + And where a generic subclass is given: + ```py + _T = TypeVar('_T') + class MyResponse(Foo[_T]): + ... + + extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes + ``` + """ + cls = cast(object, get_origin(typ) or typ) + if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains] + # we're given the class directly + return extract_type_arg(typ, index) + + # if a subclass is given + # --- + # this is needed as __orig_bases__ is not present in the typeshed stubs + # because it is intended to be for internal use only, however there does + # not seem to be a way to resolve generic TypeVars for inherited subclasses + # without using it. + if isinstance(cls, InheritsGeneric): + target_base_class: Any | None = None + for base in cls.__orig_bases__: + if base.__origin__ in generic_bases: + target_base_class = base + break + + if target_base_class is None: + raise RuntimeError( + "Could not find the generic base class;\n" + "This should never happen;\n" + f"Does {cls} inherit from one of {generic_bases} ?" + ) + + extracted = extract_type_arg(target_base_class, index) + if is_typevar(extracted): + # If the extracted type argument is itself a type variable + # then that means the subclass itself is generic, so we have + # to resolve the type argument from the class itself, not + # the base class. + # + # Note: if there is more than 1 type argument, the subclass could + # change the ordering of the type arguments, this is not currently + # supported. + return extract_type_arg(typ, index) + + return extracted + + raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") diff --git a/src/cas_parser/_utils/_utils.py b/src/cas_parser/_utils/_utils.py new file mode 100644 index 0000000..ea3cf3f --- /dev/null +++ b/src/cas_parser/_utils/_utils.py @@ -0,0 +1,422 @@ +from __future__ import annotations + +import os +import re +import inspect +import functools +from typing import ( + Any, + Tuple, + Mapping, + TypeVar, + Callable, + Iterable, + Sequence, + cast, + overload, +) +from pathlib import Path +from datetime import date, datetime +from typing_extensions import TypeGuard + +import sniffio + +from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike +from .._compat import parse_date as parse_date, parse_datetime as parse_datetime + +_T = TypeVar("_T") +_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) +_MappingT = TypeVar("_MappingT", bound=Mapping[str, object]) +_SequenceT = TypeVar("_SequenceT", bound=Sequence[object]) +CallableT = TypeVar("CallableT", bound=Callable[..., Any]) + + +def flatten(t: Iterable[Iterable[_T]]) -> list[_T]: + return [item for sublist in t for item in sublist] + + +def extract_files( + # TODO: this needs to take Dict but variance issues..... + # create protocol type ? + query: Mapping[str, object], + *, + paths: Sequence[Sequence[str]], +) -> list[tuple[str, FileTypes]]: + """Recursively extract files from the given dictionary based on specified paths. + + A path may look like this ['foo', 'files', '', 'data']. + + Note: this mutates the given dictionary. + """ + files: list[tuple[str, FileTypes]] = [] + for path in paths: + files.extend(_extract_items(query, path, index=0, flattened_key=None)) + return files + + +def _extract_items( + obj: object, + path: Sequence[str], + *, + index: int, + flattened_key: str | None, +) -> list[tuple[str, FileTypes]]: + try: + key = path[index] + except IndexError: + if isinstance(obj, NotGiven): + # no value was provided - we can safely ignore + return [] + + # cyclical import + from .._files import assert_is_file_content + + # We have exhausted the path, return the entry we found. + assert flattened_key is not None + + if is_list(obj): + files: list[tuple[str, FileTypes]] = [] + for entry in obj: + assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") + files.append((flattened_key + "[]", cast(FileTypes, entry))) + return files + + assert_is_file_content(obj, key=flattened_key) + return [(flattened_key, cast(FileTypes, obj))] + + index += 1 + if is_dict(obj): + try: + # We are at the last entry in the path so we must remove the field + if (len(path)) == index: + item = obj.pop(key) + else: + item = obj[key] + except KeyError: + # Key was not present in the dictionary, this is not indicative of an error + # as the given path may not point to a required field. We also do not want + # to enforce required fields as the API may differ from the spec in some cases. + return [] + if flattened_key is None: + flattened_key = key + else: + flattened_key += f"[{key}]" + return _extract_items( + item, + path, + index=index, + flattened_key=flattened_key, + ) + elif is_list(obj): + if key != "": + return [] + + return flatten( + [ + _extract_items( + item, + path, + index=index, + flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + ) + for item in obj + ] + ) + + # Something unexpected was passed, just ignore it. + return [] + + +def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]: + return not isinstance(obj, NotGiven) + + +# Type safe methods for narrowing types with TypeVars. +# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], +# however this cause Pyright to rightfully report errors. As we know we don't +# care about the contained types we can safely use `object` in it's place. +# +# There are two separate functions defined, `is_*` and `is_*_t` for different use cases. +# `is_*` is for when you're dealing with an unknown input +# `is_*_t` is for when you're narrowing a known union type to a specific subset + + +def is_tuple(obj: object) -> TypeGuard[tuple[object, ...]]: + return isinstance(obj, tuple) + + +def is_tuple_t(obj: _TupleT | object) -> TypeGuard[_TupleT]: + return isinstance(obj, tuple) + + +def is_sequence(obj: object) -> TypeGuard[Sequence[object]]: + return isinstance(obj, Sequence) + + +def is_sequence_t(obj: _SequenceT | object) -> TypeGuard[_SequenceT]: + return isinstance(obj, Sequence) + + +def is_mapping(obj: object) -> TypeGuard[Mapping[str, object]]: + return isinstance(obj, Mapping) + + +def is_mapping_t(obj: _MappingT | object) -> TypeGuard[_MappingT]: + return isinstance(obj, Mapping) + + +def is_dict(obj: object) -> TypeGuard[dict[object, object]]: + return isinstance(obj, dict) + + +def is_list(obj: object) -> TypeGuard[list[object]]: + return isinstance(obj, list) + + +def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: + return isinstance(obj, Iterable) + + +def deepcopy_minimal(item: _T) -> _T: + """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: + + - mappings, e.g. `dict` + - list + + This is done for performance reasons. + """ + if is_mapping(item): + return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) + if is_list(item): + return cast(_T, [deepcopy_minimal(entry) for entry in item]) + return item + + +# copied from https://github.com/Rapptz/RoboDanny +def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: + size = len(seq) + if size == 0: + return "" + + if size == 1: + return seq[0] + + if size == 2: + return f"{seq[0]} {final} {seq[1]}" + + return delim.join(seq[:-1]) + f" {final} {seq[-1]}" + + +def quote(string: str) -> str: + """Add single quotation marks around the given string. Does *not* do any escaping.""" + return f"'{string}'" + + +def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]: + """Decorator to enforce a given set of arguments or variants of arguments are passed to the decorated function. + + Useful for enforcing runtime validation of overloaded functions. + + Example usage: + ```py + @overload + def foo(*, a: str) -> str: ... + + + @overload + def foo(*, b: bool) -> str: ... + + + # This enforces the same constraints that a static type checker would + # i.e. that either a or b must be passed to the function + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: bool | None = None) -> str: ... + ``` + """ + + def inner(func: CallableT) -> CallableT: + params = inspect.signature(func).parameters + positional = [ + name + for name, param in params.items() + if param.kind + in { + param.POSITIONAL_ONLY, + param.POSITIONAL_OR_KEYWORD, + } + ] + + @functools.wraps(func) + def wrapper(*args: object, **kwargs: object) -> object: + given_params: set[str] = set() + for i, _ in enumerate(args): + try: + given_params.add(positional[i]) + except IndexError: + raise TypeError( + f"{func.__name__}() takes {len(positional)} argument(s) but {len(args)} were given" + ) from None + + for key in kwargs.keys(): + given_params.add(key) + + for variant in variants: + matches = all((param in given_params for param in variant)) + if matches: + break + else: # no break + if len(variants) > 1: + variations = human_join( + ["(" + human_join([quote(arg) for arg in variant], final="and") + ")" for variant in variants] + ) + msg = f"Missing required arguments; Expected either {variations} arguments to be given" + else: + assert len(variants) > 0 + + # TODO: this error message is not deterministic + missing = list(set(variants[0]) - given_params) + if len(missing) > 1: + msg = f"Missing required arguments: {human_join([quote(arg) for arg in missing])}" + else: + msg = f"Missing required argument: {quote(missing[0])}" + raise TypeError(msg) + return func(*args, **kwargs) + + return wrapper # type: ignore + + return inner + + +_K = TypeVar("_K") +_V = TypeVar("_V") + + +@overload +def strip_not_given(obj: None) -> None: ... + + +@overload +def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: ... + + +@overload +def strip_not_given(obj: object) -> object: ... + + +def strip_not_given(obj: object | None) -> object: + """Remove all top-level keys where their values are instances of `NotGiven`""" + if obj is None: + return None + + if not is_mapping(obj): + return obj + + return {key: value for key, value in obj.items() if not isinstance(value, NotGiven)} + + +def coerce_integer(val: str) -> int: + return int(val, base=10) + + +def coerce_float(val: str) -> float: + return float(val) + + +def coerce_boolean(val: str) -> bool: + return val == "true" or val == "1" or val == "on" + + +def maybe_coerce_integer(val: str | None) -> int | None: + if val is None: + return None + return coerce_integer(val) + + +def maybe_coerce_float(val: str | None) -> float | None: + if val is None: + return None + return coerce_float(val) + + +def maybe_coerce_boolean(val: str | None) -> bool | None: + if val is None: + return None + return coerce_boolean(val) + + +def removeprefix(string: str, prefix: str) -> str: + """Remove a prefix from a string. + + Backport of `str.removeprefix` for Python < 3.9 + """ + if string.startswith(prefix): + return string[len(prefix) :] + return string + + +def removesuffix(string: str, suffix: str) -> str: + """Remove a suffix from a string. + + Backport of `str.removesuffix` for Python < 3.9 + """ + if string.endswith(suffix): + return string[: -len(suffix)] + return string + + +def file_from_path(path: str) -> FileTypes: + contents = Path(path).read_bytes() + file_name = os.path.basename(path) + return (file_name, contents) + + +def get_required_header(headers: HeadersLike, header: str) -> str: + lower_header = header.lower() + if is_mapping_t(headers): + # mypy doesn't understand the type narrowing here + for k, v in headers.items(): # type: ignore + if k.lower() == lower_header and isinstance(v, str): + return v + + # to deal with the case where the header looks like Stainless-Event-Id + intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize()) + + for normalized_header in [header, lower_header, header.upper(), intercaps_header]: + value = headers.get(normalized_header) + if value: + return value + + raise ValueError(f"Could not find {header} header") + + +def get_async_library() -> str: + try: + return sniffio.current_async_library() + except Exception: + return "false" + + +def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]: + """A version of functools.lru_cache that retains the type signature + for the wrapped function arguments. + """ + wrapper = functools.lru_cache( # noqa: TID251 + maxsize=maxsize, + ) + return cast(Any, wrapper) # type: ignore[no-any-return] + + +def json_safe(data: object) -> object: + """Translates a mapping / sequence recursively in the same fashion + as `pydantic` v2's `model_dump(mode="json")`. + """ + if is_mapping(data): + return {json_safe(key): json_safe(value) for key, value in data.items()} + + if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)): + return [json_safe(item) for item in data] + + if isinstance(data, (datetime, date)): + return data.isoformat() + + return data diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py new file mode 100644 index 0000000..96a609e --- /dev/null +++ b/src/cas_parser/_version.py @@ -0,0 +1,4 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +__title__ = "cas_parser" +__version__ = "0.0.1" diff --git a/src/cas_parser/lib/.keep b/src/cas_parser/lib/.keep new file mode 100644 index 0000000..5e2c99f --- /dev/null +++ b/src/cas_parser/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/cas_parser/py.typed b/src/cas_parser/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/cas_parser/resources/__init__.py b/src/cas_parser/resources/__init__.py new file mode 100644 index 0000000..f1bb2bf --- /dev/null +++ b/src/cas_parser/resources/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .cas_parser import ( + CasParserResource, + AsyncCasParserResource, + CasParserResourceWithRawResponse, + AsyncCasParserResourceWithRawResponse, + CasParserResourceWithStreamingResponse, + AsyncCasParserResourceWithStreamingResponse, +) +from .cas_generator import ( + CasGeneratorResource, + AsyncCasGeneratorResource, + CasGeneratorResourceWithRawResponse, + AsyncCasGeneratorResourceWithRawResponse, + CasGeneratorResourceWithStreamingResponse, + AsyncCasGeneratorResourceWithStreamingResponse, +) + +__all__ = [ + "CasParserResource", + "AsyncCasParserResource", + "CasParserResourceWithRawResponse", + "AsyncCasParserResourceWithRawResponse", + "CasParserResourceWithStreamingResponse", + "AsyncCasParserResourceWithStreamingResponse", + "CasGeneratorResource", + "AsyncCasGeneratorResource", + "CasGeneratorResourceWithRawResponse", + "AsyncCasGeneratorResourceWithRawResponse", + "CasGeneratorResourceWithStreamingResponse", + "AsyncCasGeneratorResourceWithStreamingResponse", +] diff --git a/src/cas_parser/resources/cas_generator.py b/src/cas_parser/resources/cas_generator.py new file mode 100644 index 0000000..77bc291 --- /dev/null +++ b/src/cas_parser/resources/cas_generator.py @@ -0,0 +1,225 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..types import cas_generator_generate_cas_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.cas_generator_generate_cas_response import CasGeneratorGenerateCasResponse + +__all__ = ["CasGeneratorResource", "AsyncCasGeneratorResource"] + + +class CasGeneratorResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CasGeneratorResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return CasGeneratorResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasGeneratorResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return CasGeneratorResourceWithStreamingResponse(self) + + def generate_cas( + self, + *, + email: str, + from_date: str, + password: str, + to_date: str, + cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] | NotGiven = NOT_GIVEN, + pan_no: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasGeneratorGenerateCasResponse: + """ + This endpoint generates CAS (Consolidated Account Statement) documents by + submitting a mailback request to the specified CAS authority. Currently only + supports KFintech, with plans to support CAMS, CDSL, and NSDL in the future. + + Args: + email: Email address to receive the CAS document + + from_date: Start date for the CAS period (format YYYY-MM-DD) + + password: Password to protect the generated CAS PDF + + to_date: End date for the CAS period (format YYYY-MM-DD) + + cas_authority: CAS authority to generate the document from (currently only kfintech is + supported) + + pan_no: PAN number (optional for some CAS authorities) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v4/generate", + body=maybe_transform( + { + "email": email, + "from_date": from_date, + "password": password, + "to_date": to_date, + "cas_authority": cas_authority, + "pan_no": pan_no, + }, + cas_generator_generate_cas_params.CasGeneratorGenerateCasParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CasGeneratorGenerateCasResponse, + ) + + +class AsyncCasGeneratorResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCasGeneratorResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncCasGeneratorResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasGeneratorResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncCasGeneratorResourceWithStreamingResponse(self) + + async def generate_cas( + self, + *, + email: str, + from_date: str, + password: str, + to_date: str, + cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] | NotGiven = NOT_GIVEN, + pan_no: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasGeneratorGenerateCasResponse: + """ + This endpoint generates CAS (Consolidated Account Statement) documents by + submitting a mailback request to the specified CAS authority. Currently only + supports KFintech, with plans to support CAMS, CDSL, and NSDL in the future. + + Args: + email: Email address to receive the CAS document + + from_date: Start date for the CAS period (format YYYY-MM-DD) + + password: Password to protect the generated CAS PDF + + to_date: End date for the CAS period (format YYYY-MM-DD) + + cas_authority: CAS authority to generate the document from (currently only kfintech is + supported) + + pan_no: PAN number (optional for some CAS authorities) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v4/generate", + body=await async_maybe_transform( + { + "email": email, + "from_date": from_date, + "password": password, + "to_date": to_date, + "cas_authority": cas_authority, + "pan_no": pan_no, + }, + cas_generator_generate_cas_params.CasGeneratorGenerateCasParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CasGeneratorGenerateCasResponse, + ) + + +class CasGeneratorResourceWithRawResponse: + def __init__(self, cas_generator: CasGeneratorResource) -> None: + self._cas_generator = cas_generator + + self.generate_cas = to_raw_response_wrapper( + cas_generator.generate_cas, + ) + + +class AsyncCasGeneratorResourceWithRawResponse: + def __init__(self, cas_generator: AsyncCasGeneratorResource) -> None: + self._cas_generator = cas_generator + + self.generate_cas = async_to_raw_response_wrapper( + cas_generator.generate_cas, + ) + + +class CasGeneratorResourceWithStreamingResponse: + def __init__(self, cas_generator: CasGeneratorResource) -> None: + self._cas_generator = cas_generator + + self.generate_cas = to_streamed_response_wrapper( + cas_generator.generate_cas, + ) + + +class AsyncCasGeneratorResourceWithStreamingResponse: + def __init__(self, cas_generator: AsyncCasGeneratorResource) -> None: + self._cas_generator = cas_generator + + self.generate_cas = async_to_streamed_response_wrapper( + cas_generator.generate_cas, + ) diff --git a/src/cas_parser/resources/cas_parser.py b/src/cas_parser/resources/cas_parser.py new file mode 100644 index 0000000..45d205a --- /dev/null +++ b/src/cas_parser/resources/cas_parser.py @@ -0,0 +1,592 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast + +import httpx + +from ..types import ( + cas_parser_cdsl_params, + cas_parser_nsdl_params, + cas_parser_smart_parse_params, + cas_parser_cams_kfintech_params, +) +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.unified_response import UnifiedResponse + +__all__ = ["CasParserResource", "AsyncCasParserResource"] + + +class CasParserResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CasParserResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return CasParserResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasParserResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return CasParserResourceWithStreamingResponse(self) + + def cams_kfintech( + self, + *, + password: str | NotGiven = NOT_GIVEN, + pdf_file: str | NotGiven = NOT_GIVEN, + pdf_url: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UnifiedResponse: + """ + This endpoint specifically parses CAMS/KFintech CAS (Consolidated Account + Statement) PDF files and returns data in a unified format. Use this endpoint + when you know the PDF is from CAMS or KFintech. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file + + pdf_url: URL to the CAS PDF file + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v4/cams_kfintech/parse", + body=maybe_transform(body, cas_parser_cams_kfintech_params.CasParserCamsKfintechParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + def cdsl( + self, + *, + password: str | NotGiven = NOT_GIVEN, + pdf_file: str | NotGiven = NOT_GIVEN, + pdf_url: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UnifiedResponse: + """ + This endpoint specifically parses CDSL CAS (Consolidated Account Statement) PDF + files and returns data in a unified format. Use this endpoint when you know the + PDF is from CDSL. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file + + pdf_url: URL to the CAS PDF file + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v4/cdsl/parse", + body=maybe_transform(body, cas_parser_cdsl_params.CasParserCdslParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + def nsdl( + self, + *, + password: str | NotGiven = NOT_GIVEN, + pdf_file: str | NotGiven = NOT_GIVEN, + pdf_url: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UnifiedResponse: + """ + This endpoint specifically parses NSDL CAS (Consolidated Account Statement) PDF + files and returns data in a unified format. Use this endpoint when you know the + PDF is from NSDL. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file + + pdf_url: URL to the CAS PDF file + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v4/nsdl/parse", + body=maybe_transform(body, cas_parser_nsdl_params.CasParserNsdlParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + def smart_parse( + self, + *, + password: str | NotGiven = NOT_GIVEN, + pdf_file: str | NotGiven = NOT_GIVEN, + pdf_url: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UnifiedResponse: + """ + This endpoint parses CAS (Consolidated Account Statement) PDF files from NSDL, + CDSL, or CAMS/KFintech and returns data in a unified format. It auto-detects the + CAS type and transforms the data into a consistent structure regardless of the + source. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file + + pdf_url: URL to the CAS PDF file + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v4/smart/parse", + body=maybe_transform(body, cas_parser_smart_parse_params.CasParserSmartParseParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + +class AsyncCasParserResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCasParserResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncCasParserResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasParserResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncCasParserResourceWithStreamingResponse(self) + + async def cams_kfintech( + self, + *, + password: str | NotGiven = NOT_GIVEN, + pdf_file: str | NotGiven = NOT_GIVEN, + pdf_url: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UnifiedResponse: + """ + This endpoint specifically parses CAMS/KFintech CAS (Consolidated Account + Statement) PDF files and returns data in a unified format. Use this endpoint + when you know the PDF is from CAMS or KFintech. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file + + pdf_url: URL to the CAS PDF file + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v4/cams_kfintech/parse", + body=await async_maybe_transform(body, cas_parser_cams_kfintech_params.CasParserCamsKfintechParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + async def cdsl( + self, + *, + password: str | NotGiven = NOT_GIVEN, + pdf_file: str | NotGiven = NOT_GIVEN, + pdf_url: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UnifiedResponse: + """ + This endpoint specifically parses CDSL CAS (Consolidated Account Statement) PDF + files and returns data in a unified format. Use this endpoint when you know the + PDF is from CDSL. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file + + pdf_url: URL to the CAS PDF file + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v4/cdsl/parse", + body=await async_maybe_transform(body, cas_parser_cdsl_params.CasParserCdslParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + async def nsdl( + self, + *, + password: str | NotGiven = NOT_GIVEN, + pdf_file: str | NotGiven = NOT_GIVEN, + pdf_url: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UnifiedResponse: + """ + This endpoint specifically parses NSDL CAS (Consolidated Account Statement) PDF + files and returns data in a unified format. Use this endpoint when you know the + PDF is from NSDL. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file + + pdf_url: URL to the CAS PDF file + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v4/nsdl/parse", + body=await async_maybe_transform(body, cas_parser_nsdl_params.CasParserNsdlParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + async def smart_parse( + self, + *, + password: str | NotGiven = NOT_GIVEN, + pdf_file: str | NotGiven = NOT_GIVEN, + pdf_url: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UnifiedResponse: + """ + This endpoint parses CAS (Consolidated Account Statement) PDF files from NSDL, + CDSL, or CAMS/KFintech and returns data in a unified format. It auto-detects the + CAS type and transforms the data into a consistent structure regardless of the + source. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file + + pdf_url: URL to the CAS PDF file + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v4/smart/parse", + body=await async_maybe_transform(body, cas_parser_smart_parse_params.CasParserSmartParseParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + +class CasParserResourceWithRawResponse: + def __init__(self, cas_parser: CasParserResource) -> None: + self._cas_parser = cas_parser + + self.cams_kfintech = to_raw_response_wrapper( + cas_parser.cams_kfintech, + ) + self.cdsl = to_raw_response_wrapper( + cas_parser.cdsl, + ) + self.nsdl = to_raw_response_wrapper( + cas_parser.nsdl, + ) + self.smart_parse = to_raw_response_wrapper( + cas_parser.smart_parse, + ) + + +class AsyncCasParserResourceWithRawResponse: + def __init__(self, cas_parser: AsyncCasParserResource) -> None: + self._cas_parser = cas_parser + + self.cams_kfintech = async_to_raw_response_wrapper( + cas_parser.cams_kfintech, + ) + self.cdsl = async_to_raw_response_wrapper( + cas_parser.cdsl, + ) + self.nsdl = async_to_raw_response_wrapper( + cas_parser.nsdl, + ) + self.smart_parse = async_to_raw_response_wrapper( + cas_parser.smart_parse, + ) + + +class CasParserResourceWithStreamingResponse: + def __init__(self, cas_parser: CasParserResource) -> None: + self._cas_parser = cas_parser + + self.cams_kfintech = to_streamed_response_wrapper( + cas_parser.cams_kfintech, + ) + self.cdsl = to_streamed_response_wrapper( + cas_parser.cdsl, + ) + self.nsdl = to_streamed_response_wrapper( + cas_parser.nsdl, + ) + self.smart_parse = to_streamed_response_wrapper( + cas_parser.smart_parse, + ) + + +class AsyncCasParserResourceWithStreamingResponse: + def __init__(self, cas_parser: AsyncCasParserResource) -> None: + self._cas_parser = cas_parser + + self.cams_kfintech = async_to_streamed_response_wrapper( + cas_parser.cams_kfintech, + ) + self.cdsl = async_to_streamed_response_wrapper( + cas_parser.cdsl, + ) + self.nsdl = async_to_streamed_response_wrapper( + cas_parser.nsdl, + ) + self.smart_parse = async_to_streamed_response_wrapper( + cas_parser.smart_parse, + ) diff --git a/src/cas_parser/types/__init__.py b/src/cas_parser/types/__init__.py new file mode 100644 index 0000000..4dbdba1 --- /dev/null +++ b/src/cas_parser/types/__init__.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .unified_response import UnifiedResponse as UnifiedResponse +from .cas_parser_cdsl_params import CasParserCdslParams as CasParserCdslParams +from .cas_parser_nsdl_params import CasParserNsdlParams as CasParserNsdlParams +from .cas_parser_smart_parse_params import CasParserSmartParseParams as CasParserSmartParseParams +from .cas_parser_cams_kfintech_params import CasParserCamsKfintechParams as CasParserCamsKfintechParams +from .cas_generator_generate_cas_params import CasGeneratorGenerateCasParams as CasGeneratorGenerateCasParams +from .cas_generator_generate_cas_response import CasGeneratorGenerateCasResponse as CasGeneratorGenerateCasResponse diff --git a/src/cas_parser/types/cas_generator_generate_cas_params.py b/src/cas_parser/types/cas_generator_generate_cas_params.py new file mode 100644 index 0000000..253dcea --- /dev/null +++ b/src/cas_parser/types/cas_generator_generate_cas_params.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["CasGeneratorGenerateCasParams"] + + +class CasGeneratorGenerateCasParams(TypedDict, total=False): + email: Required[str] + """Email address to receive the CAS document""" + + from_date: Required[str] + """Start date for the CAS period (format YYYY-MM-DD)""" + + password: Required[str] + """Password to protect the generated CAS PDF""" + + to_date: Required[str] + """End date for the CAS period (format YYYY-MM-DD)""" + + cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] + """ + CAS authority to generate the document from (currently only kfintech is + supported) + """ + + pan_no: str + """PAN number (optional for some CAS authorities)""" diff --git a/src/cas_parser/types/cas_generator_generate_cas_response.py b/src/cas_parser/types/cas_generator_generate_cas_response.py new file mode 100644 index 0000000..e781ef9 --- /dev/null +++ b/src/cas_parser/types/cas_generator_generate_cas_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["CasGeneratorGenerateCasResponse"] + + +class CasGeneratorGenerateCasResponse(BaseModel): + msg: Optional[str] = None + + status: Optional[str] = None diff --git a/src/cas_parser/types/cas_parser_cams_kfintech_params.py b/src/cas_parser/types/cas_parser_cams_kfintech_params.py new file mode 100644 index 0000000..69b8597 --- /dev/null +++ b/src/cas_parser/types/cas_parser_cams_kfintech_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["CasParserCamsKfintechParams"] + + +class CasParserCamsKfintechParams(TypedDict, total=False): + password: str + """Password for the PDF file (if required)""" + + pdf_file: str + """Base64 encoded CAS PDF file""" + + pdf_url: str + """URL to the CAS PDF file""" diff --git a/src/cas_parser/types/cas_parser_cdsl_params.py b/src/cas_parser/types/cas_parser_cdsl_params.py new file mode 100644 index 0000000..a25130c --- /dev/null +++ b/src/cas_parser/types/cas_parser_cdsl_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["CasParserCdslParams"] + + +class CasParserCdslParams(TypedDict, total=False): + password: str + """Password for the PDF file (if required)""" + + pdf_file: str + """Base64 encoded CAS PDF file""" + + pdf_url: str + """URL to the CAS PDF file""" diff --git a/src/cas_parser/types/cas_parser_nsdl_params.py b/src/cas_parser/types/cas_parser_nsdl_params.py new file mode 100644 index 0000000..51177cb --- /dev/null +++ b/src/cas_parser/types/cas_parser_nsdl_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["CasParserNsdlParams"] + + +class CasParserNsdlParams(TypedDict, total=False): + password: str + """Password for the PDF file (if required)""" + + pdf_file: str + """Base64 encoded CAS PDF file""" + + pdf_url: str + """URL to the CAS PDF file""" diff --git a/src/cas_parser/types/cas_parser_smart_parse_params.py b/src/cas_parser/types/cas_parser_smart_parse_params.py new file mode 100644 index 0000000..003e28d --- /dev/null +++ b/src/cas_parser/types/cas_parser_smart_parse_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["CasParserSmartParseParams"] + + +class CasParserSmartParseParams(TypedDict, total=False): + password: str + """Password for the PDF file (if required)""" + + pdf_file: str + """Base64 encoded CAS PDF file""" + + pdf_url: str + """URL to the CAS PDF file""" diff --git a/src/cas_parser/types/unified_response.py b/src/cas_parser/types/unified_response.py new file mode 100644 index 0000000..7dc5439 --- /dev/null +++ b/src/cas_parser/types/unified_response.py @@ -0,0 +1,432 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import datetime +from typing import List, Optional +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = [ + "UnifiedResponse", + "DematAccount", + "DematAccountAdditionalInfo", + "DematAccountHoldings", + "DematAccountHoldingsAif", + "DematAccountHoldingsCorporateBond", + "DematAccountHoldingsDematMutualFund", + "DematAccountHoldingsEquity", + "DematAccountHoldingsGovernmentSecurity", + "Insurance", + "InsuranceLifeInsurancePolicy", + "Investor", + "Meta", + "MetaStatementPeriod", + "MutualFund", + "MutualFundAdditionalInfo", + "MutualFundScheme", + "MutualFundSchemeAdditionalInfo", + "MutualFundSchemeGain", + "MutualFundSchemeTransaction", + "Summary", + "SummaryAccounts", + "SummaryAccountsDemat", + "SummaryAccountsInsurance", + "SummaryAccountsMutualFunds", +] + + +class DematAccountAdditionalInfo(BaseModel): + bo_status: Optional[str] = None + """Beneficiary Owner status (CDSL)""" + + bo_sub_status: Optional[str] = None + """Beneficiary Owner sub-status (CDSL)""" + + bo_type: Optional[str] = None + """Beneficiary Owner type (CDSL)""" + + bsda: Optional[str] = None + """Basic Services Demat Account status (CDSL)""" + + email: Optional[str] = None + """Email associated with the demat account (CDSL)""" + + linked_pans: Optional[List[str]] = None + """List of linked PAN numbers (NSDL)""" + + nominee: Optional[str] = None + """Nominee details (CDSL)""" + + status: Optional[str] = None + """Account status (CDSL)""" + + +class DematAccountHoldingsAif(BaseModel): + additional_info: Optional[object] = None + """Additional information specific to the AIF""" + + isin: Optional[str] = None + """ISIN code of the AIF""" + + name: Optional[str] = None + """Name of the AIF""" + + units: Optional[float] = None + """Number of units held""" + + value: Optional[float] = None + """Current market value of the holding""" + + +class DematAccountHoldingsCorporateBond(BaseModel): + additional_info: Optional[object] = None + """Additional information specific to the corporate bond""" + + isin: Optional[str] = None + """ISIN code of the corporate bond""" + + name: Optional[str] = None + """Name of the corporate bond""" + + units: Optional[float] = None + """Number of units held""" + + value: Optional[float] = None + """Current market value of the holding""" + + +class DematAccountHoldingsDematMutualFund(BaseModel): + additional_info: Optional[object] = None + """Additional information specific to the mutual fund""" + + isin: Optional[str] = None + """ISIN code of the mutual fund""" + + name: Optional[str] = None + """Name of the mutual fund""" + + units: Optional[float] = None + """Number of units held""" + + value: Optional[float] = None + """Current market value of the holding""" + + +class DematAccountHoldingsEquity(BaseModel): + additional_info: Optional[object] = None + """Additional information specific to the equity""" + + isin: Optional[str] = None + """ISIN code of the equity""" + + name: Optional[str] = None + """Name of the equity""" + + units: Optional[float] = None + """Number of units held""" + + value: Optional[float] = None + """Current market value of the holding""" + + +class DematAccountHoldingsGovernmentSecurity(BaseModel): + additional_info: Optional[object] = None + """Additional information specific to the government security""" + + isin: Optional[str] = None + """ISIN code of the government security""" + + name: Optional[str] = None + """Name of the government security""" + + units: Optional[float] = None + """Number of units held""" + + value: Optional[float] = None + """Current market value of the holding""" + + +class DematAccountHoldings(BaseModel): + aifs: Optional[List[DematAccountHoldingsAif]] = None + + corporate_bonds: Optional[List[DematAccountHoldingsCorporateBond]] = None + + demat_mutual_funds: Optional[List[DematAccountHoldingsDematMutualFund]] = None + + equities: Optional[List[DematAccountHoldingsEquity]] = None + + government_securities: Optional[List[DematAccountHoldingsGovernmentSecurity]] = None + + +class DematAccount(BaseModel): + additional_info: Optional[DematAccountAdditionalInfo] = None + """Additional information specific to the demat account type""" + + bo_id: Optional[str] = None + """Beneficiary Owner ID (primarily for CDSL)""" + + client_id: Optional[str] = None + """Client ID""" + + demat_type: Optional[Literal["NSDL", "CDSL"]] = None + """Type of demat account""" + + dp_id: Optional[str] = None + """Depository Participant ID""" + + dp_name: Optional[str] = None + """Depository Participant name""" + + holdings: Optional[DematAccountHoldings] = None + + value: Optional[float] = None + """Total value of the demat account""" + + +class InsuranceLifeInsurancePolicy(BaseModel): + additional_info: Optional[object] = None + """Additional information specific to the policy""" + + life_assured: Optional[str] = None + """Name of the life assured""" + + policy_name: Optional[str] = None + """Name of the insurance policy""" + + policy_number: Optional[str] = None + """Insurance policy number""" + + premium_amount: Optional[float] = None + """Premium amount""" + + premium_frequency: Optional[str] = None + """Frequency of premium payment (e.g., Annual, Monthly)""" + + provider: Optional[str] = None + """Insurance company name""" + + status: Optional[str] = None + """Status of the policy (e.g., Active, Lapsed)""" + + sum_assured: Optional[float] = None + """Sum assured amount""" + + +class Insurance(BaseModel): + life_insurance_policies: Optional[List[InsuranceLifeInsurancePolicy]] = None + + +class Investor(BaseModel): + address: Optional[str] = None + """Address of the investor""" + + cas_id: Optional[str] = None + """CAS ID of the investor (only for NSDL and CDSL)""" + + email: Optional[str] = None + """Email address of the investor""" + + mobile: Optional[str] = None + """Mobile number of the investor""" + + name: Optional[str] = None + """Name of the investor""" + + pan: Optional[str] = None + """PAN (Permanent Account Number) of the investor""" + + pincode: Optional[str] = None + """Postal code of the investor's address""" + + +class MetaStatementPeriod(BaseModel): + from_: Optional[datetime.date] = FieldInfo(alias="from", default=None) + """Start date of the statement period""" + + to: Optional[datetime.date] = None + """End date of the statement period""" + + +class Meta(BaseModel): + cas_type: Optional[Literal["NSDL", "CDSL", "CAMS_KFINTECH"]] = None + """Type of CAS detected and processed""" + + generated_at: Optional[datetime.datetime] = None + """Timestamp when the response was generated""" + + statement_period: Optional[MetaStatementPeriod] = None + + +class MutualFundAdditionalInfo(BaseModel): + kyc: Optional[str] = None + """KYC status of the folio""" + + pan: Optional[str] = None + """PAN associated with the folio""" + + pankyc: Optional[str] = None + """PAN KYC status""" + + +class MutualFundSchemeAdditionalInfo(BaseModel): + advisor: Optional[str] = None + """Financial advisor name (CAMS/KFintech)""" + + amfi: Optional[str] = None + """AMFI code for the scheme (CAMS/KFintech)""" + + close_units: Optional[float] = None + """Closing balance units (CAMS/KFintech)""" + + open_units: Optional[float] = None + """Opening balance units (CAMS/KFintech)""" + + rta_code: Optional[str] = None + """RTA code for the scheme (CAMS/KFintech)""" + + +class MutualFundSchemeGain(BaseModel): + absolute: Optional[float] = None + """Absolute gain or loss""" + + percentage: Optional[float] = None + """Percentage gain or loss""" + + +class MutualFundSchemeTransaction(BaseModel): + amount: Optional[float] = None + """Transaction amount""" + + balance: Optional[float] = None + """Balance units after transaction""" + + date: Optional[datetime.date] = None + """Transaction date""" + + description: Optional[str] = None + """Transaction description""" + + dividend_rate: Optional[float] = None + """Dividend rate (for dividend transactions)""" + + nav: Optional[float] = None + """NAV on transaction date""" + + type: Optional[str] = None + """Transaction type detected based on description. + + Possible values are + PURCHASE,PURCHASE_SIP,REDEMPTION,SWITCH_IN,SWITCH_IN_MERGER,SWITCH_OUT,SWITCH_OUT_MERGER,DIVIDEND_PAYOUT,DIVIDEND_REINVESTMENT,SEGREGATION,STAMP_DUTY_TAX,TDS_TAX,STT_TAX,MISC. + If dividend_rate is present, then possible values are dividend_rate is + applicable only for DIVIDEND_PAYOUT and DIVIDEND_REINVESTMENT. + """ + + units: Optional[float] = None + """Number of units involved""" + + +class MutualFundScheme(BaseModel): + additional_info: Optional[MutualFundSchemeAdditionalInfo] = None + """Additional information specific to the scheme""" + + cost: Optional[float] = None + """Cost of investment""" + + gain: Optional[MutualFundSchemeGain] = None + + isin: Optional[str] = None + """ISIN code of the scheme""" + + name: Optional[str] = None + """Scheme name""" + + nav: Optional[float] = None + """Net Asset Value per unit""" + + nominees: Optional[List[str]] = None + """List of nominees""" + + transactions: Optional[List[MutualFundSchemeTransaction]] = None + + type: Optional[Literal["Equity", "Debt", "Hybrid", "Other"]] = None + """Type of mutual fund scheme""" + + units: Optional[float] = None + """Number of units held""" + + value: Optional[float] = None + """Current market value of the holding""" + + +class MutualFund(BaseModel): + additional_info: Optional[MutualFundAdditionalInfo] = None + """Additional folio information""" + + amc: Optional[str] = None + """Asset Management Company name""" + + folio_number: Optional[str] = None + """Folio number""" + + registrar: Optional[str] = None + """Registrar and Transfer Agent name""" + + schemes: Optional[List[MutualFundScheme]] = None + + value: Optional[float] = None + """Total value of the folio""" + + +class SummaryAccountsDemat(BaseModel): + count: Optional[int] = None + """Number of demat accounts""" + + total_value: Optional[float] = None + """Total value of demat accounts""" + + +class SummaryAccountsInsurance(BaseModel): + count: Optional[int] = None + """Number of insurance policies""" + + total_value: Optional[float] = None + """Total value of insurance policies""" + + +class SummaryAccountsMutualFunds(BaseModel): + count: Optional[int] = None + """Number of mutual fund folios""" + + total_value: Optional[float] = None + """Total value of mutual funds""" + + +class SummaryAccounts(BaseModel): + demat: Optional[SummaryAccountsDemat] = None + + insurance: Optional[SummaryAccountsInsurance] = None + + mutual_funds: Optional[SummaryAccountsMutualFunds] = None + + +class Summary(BaseModel): + accounts: Optional[SummaryAccounts] = None + + total_value: Optional[float] = None + """Total portfolio value across all accounts""" + + +class UnifiedResponse(BaseModel): + demat_accounts: Optional[List[DematAccount]] = None + + insurance: Optional[Insurance] = None + + investor: Optional[Investor] = None + + meta: Optional[Meta] = None + + mutual_funds: Optional[List[MutualFund]] = None + + summary: Optional[Summary] = None diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/__init__.py b/tests/api_resources/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/test_cas_generator.py b/tests/api_resources/test_cas_generator.py new file mode 100644 index 0000000..d0d591d --- /dev/null +++ b/tests/api_resources/test_cas_generator.py @@ -0,0 +1,136 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import CasGeneratorGenerateCasResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCasGenerator: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_generate_cas(self, client: CasParser) -> None: + cas_generator = client.cas_generator.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) + assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_generate_cas_with_all_params(self, client: CasParser) -> None: + cas_generator = client.cas_generator.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + cas_authority="kfintech", + pan_no="ABCDE1234F", + ) + assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_generate_cas(self, client: CasParser) -> None: + response = client.cas_generator.with_raw_response.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cas_generator = response.parse() + assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_generate_cas(self, client: CasParser) -> None: + with client.cas_generator.with_streaming_response.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cas_generator = response.parse() + assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncCasGenerator: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_generate_cas(self, async_client: AsyncCasParser) -> None: + cas_generator = await async_client.cas_generator.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) + assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_generate_cas_with_all_params(self, async_client: AsyncCasParser) -> None: + cas_generator = await async_client.cas_generator.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + cas_authority="kfintech", + pan_no="ABCDE1234F", + ) + assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_generate_cas(self, async_client: AsyncCasParser) -> None: + response = await async_client.cas_generator.with_raw_response.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cas_generator = await response.parse() + assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_generate_cas(self, async_client: AsyncCasParser) -> None: + async with async_client.cas_generator.with_streaming_response.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cas_generator = await response.parse() + assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_cas_parser.py b/tests/api_resources/test_cas_parser.py new file mode 100644 index 0000000..cf0509b --- /dev/null +++ b/tests/api_resources/test_cas_parser.py @@ -0,0 +1,330 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import ( + UnifiedResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCasParser: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_cams_kfintech(self, client: CasParser) -> None: + cas_parser = client.cas_parser.cams_kfintech() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_cams_kfintech_with_all_params(self, client: CasParser) -> None: + cas_parser = client.cas_parser.cams_kfintech( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_cams_kfintech(self, client: CasParser) -> None: + response = client.cas_parser.with_raw_response.cams_kfintech() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cas_parser = response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_cams_kfintech(self, client: CasParser) -> None: + with client.cas_parser.with_streaming_response.cams_kfintech() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cas_parser = response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_cdsl(self, client: CasParser) -> None: + cas_parser = client.cas_parser.cdsl() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_cdsl_with_all_params(self, client: CasParser) -> None: + cas_parser = client.cas_parser.cdsl( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_cdsl(self, client: CasParser) -> None: + response = client.cas_parser.with_raw_response.cdsl() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cas_parser = response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_cdsl(self, client: CasParser) -> None: + with client.cas_parser.with_streaming_response.cdsl() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cas_parser = response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_nsdl(self, client: CasParser) -> None: + cas_parser = client.cas_parser.nsdl() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_nsdl_with_all_params(self, client: CasParser) -> None: + cas_parser = client.cas_parser.nsdl( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_nsdl(self, client: CasParser) -> None: + response = client.cas_parser.with_raw_response.nsdl() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cas_parser = response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_nsdl(self, client: CasParser) -> None: + with client.cas_parser.with_streaming_response.nsdl() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cas_parser = response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_smart_parse(self, client: CasParser) -> None: + cas_parser = client.cas_parser.smart_parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_smart_parse_with_all_params(self, client: CasParser) -> None: + cas_parser = client.cas_parser.smart_parse( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_smart_parse(self, client: CasParser) -> None: + response = client.cas_parser.with_raw_response.smart_parse() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cas_parser = response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_smart_parse(self, client: CasParser) -> None: + with client.cas_parser.with_streaming_response.smart_parse() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cas_parser = response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncCasParser: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_cams_kfintech(self, async_client: AsyncCasParser) -> None: + cas_parser = await async_client.cas_parser.cams_kfintech() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_cams_kfintech_with_all_params(self, async_client: AsyncCasParser) -> None: + cas_parser = await async_client.cas_parser.cams_kfintech( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_cams_kfintech(self, async_client: AsyncCasParser) -> None: + response = await async_client.cas_parser.with_raw_response.cams_kfintech() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cas_parser = await response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_cams_kfintech(self, async_client: AsyncCasParser) -> None: + async with async_client.cas_parser.with_streaming_response.cams_kfintech() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cas_parser = await response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_cdsl(self, async_client: AsyncCasParser) -> None: + cas_parser = await async_client.cas_parser.cdsl() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_cdsl_with_all_params(self, async_client: AsyncCasParser) -> None: + cas_parser = await async_client.cas_parser.cdsl( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_cdsl(self, async_client: AsyncCasParser) -> None: + response = await async_client.cas_parser.with_raw_response.cdsl() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cas_parser = await response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_cdsl(self, async_client: AsyncCasParser) -> None: + async with async_client.cas_parser.with_streaming_response.cdsl() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cas_parser = await response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_nsdl(self, async_client: AsyncCasParser) -> None: + cas_parser = await async_client.cas_parser.nsdl() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_nsdl_with_all_params(self, async_client: AsyncCasParser) -> None: + cas_parser = await async_client.cas_parser.nsdl( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_nsdl(self, async_client: AsyncCasParser) -> None: + response = await async_client.cas_parser.with_raw_response.nsdl() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cas_parser = await response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_nsdl(self, async_client: AsyncCasParser) -> None: + async with async_client.cas_parser.with_streaming_response.nsdl() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cas_parser = await response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_smart_parse(self, async_client: AsyncCasParser) -> None: + cas_parser = await async_client.cas_parser.smart_parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_smart_parse_with_all_params(self, async_client: AsyncCasParser) -> None: + cas_parser = await async_client.cas_parser.smart_parse( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_smart_parse(self, async_client: AsyncCasParser) -> None: + response = await async_client.cas_parser.with_raw_response.smart_parse() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cas_parser = await response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_smart_parse(self, async_client: AsyncCasParser) -> None: + async with async_client.cas_parser.with_streaming_response.smart_parse() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cas_parser = await response.parse() + assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2837b59 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,84 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +import logging +from typing import TYPE_CHECKING, Iterator, AsyncIterator + +import httpx +import pytest +from pytest_asyncio import is_async_test + +from cas_parser import CasParser, AsyncCasParser, DefaultAioHttpClient +from cas_parser._utils import is_dict + +if TYPE_CHECKING: + from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] + +pytest.register_assert_rewrite("tests.utils") + +logging.getLogger("cas_parser").setLevel(logging.DEBUG) + + +# automatically add `pytest.mark.asyncio()` to all of our async tests +# so we don't have to add that boilerplate everywhere +def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: + pytest_asyncio_tests = (item for item in items if is_async_test(item)) + session_scope_marker = pytest.mark.asyncio(loop_scope="session") + for async_test in pytest_asyncio_tests: + async_test.add_marker(session_scope_marker, append=False) + + # We skip tests that use both the aiohttp client and respx_mock as respx_mock + # doesn't support custom transports. + for item in items: + if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: + continue + + if not hasattr(item, "callspec"): + continue + + async_client_param = item.callspec.params.get("async_client") + if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": + item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) + + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + +api_key = "My API Key" + + +@pytest.fixture(scope="session") +def client(request: FixtureRequest) -> Iterator[CasParser]: + strict = getattr(request, "param", True) + if not isinstance(strict, bool): + raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + + with CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + yield client + + +@pytest.fixture(scope="session") +async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncCasParser]: + param = getattr(request, "param", True) + + # defaults + strict = True + http_client: None | httpx.AsyncClient = None + + if isinstance(param, bool): + strict = param + elif is_dict(param): + strict = param.get("strict", True) + assert isinstance(strict, bool) + + http_client_type = param.get("http_client", "httpx") + if http_client_type == "aiohttp": + http_client = DefaultAioHttpClient() + else: + raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") + + async with AsyncCasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client + ) as client: + yield client diff --git a/tests/sample_file.txt b/tests/sample_file.txt new file mode 100644 index 0000000..af5626b --- /dev/null +++ b/tests/sample_file.txt @@ -0,0 +1 @@ +Hello, world! diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..5b83c72 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,1748 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import gc +import os +import sys +import json +import time +import asyncio +import inspect +import subprocess +import tracemalloc +from typing import Any, Union, cast +from textwrap import dedent +from unittest import mock +from typing_extensions import Literal + +import httpx +import pytest +from respx import MockRouter +from pydantic import ValidationError + +from cas_parser import CasParser, AsyncCasParser, APIResponseValidationError +from cas_parser._types import Omit +from cas_parser._models import BaseModel, FinalRequestOptions +from cas_parser._exceptions import APIStatusError, CasParserError, APITimeoutError, APIResponseValidationError +from cas_parser._base_client import ( + DEFAULT_TIMEOUT, + HTTPX_DEFAULT_TIMEOUT, + BaseClient, + DefaultHttpxClient, + DefaultAsyncHttpxClient, + make_request_options, +) + +from .utils import update_env + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") +api_key = "My API Key" + + +def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + return dict(url.params) + + +def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: + return 0.1 + + +def _get_open_connections(client: CasParser | AsyncCasParser) -> int: + transport = client._client._transport + assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) + + pool = transport._pool + return len(pool._requests) + + +class TestCasParser: + client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + @pytest.mark.respx(base_url=base_url) + def test_raw_response(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = self.client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + @pytest.mark.respx(base_url=base_url) + def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = self.client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + def test_copy(self) -> None: + copied = self.client.copy() + assert id(copied) != id(self.client) + + copied = self.client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert self.client.api_key == "My API Key" + + def test_copy_default_options(self) -> None: + # options that have a default are overridden correctly + copied = self.client.copy(max_retries=7) + assert copied.max_retries == 7 + assert self.client.max_retries == 2 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(self.client.timeout, httpx.Timeout) + copied = self.client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(self.client.timeout, httpx.Timeout) + + def test_copy_default_headers(self) -> None: + client = CasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + assert client.default_headers["X-Foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert copied.default_headers["X-Foo"] == "bar" + + # merges already given headers + copied = client.copy(default_headers={"X-Bar": "stainless"}) + assert copied.default_headers["X-Foo"] == "bar" + assert copied.default_headers["X-Bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_headers={"X-Foo": "stainless"}) + assert copied.default_headers["X-Foo"] == "stainless" + + # set_default_headers + + # completely overrides already set values + copied = client.copy(set_default_headers={}) + assert copied.default_headers.get("X-Foo") is None + + copied = client.copy(set_default_headers={"X-Bar": "Robert"}) + assert copied.default_headers["X-Bar"] == "Robert" + + with pytest.raises( + ValueError, + match="`default_headers` and `set_default_headers` arguments are mutually exclusive", + ): + client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + + def test_copy_default_query(self) -> None: + client = CasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + def test_copy_signature(self) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + self.client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(self.client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") + def test_copy_build_request(self) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client = self.client.copy() + client._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "cas_parser/_legacy_response.py", + "cas_parser/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "cas_parser/_compat.py", + # Standard library leaks we don't care about. + "/logging/__init__.py", + ] + ): + return + + leaks.append(diff) + + leaks: list[tracemalloc.StatisticDiff] = [] + for diff in snapshot_after.compare_to(snapshot_before, "traceback"): + add_leak(leaks, diff) + if leaks: + for leak in leaks: + print("MEMORY LEAK:", leak) + for frame in leak.traceback: + print(frame) + raise AssertionError() + + def test_request_timeout(self) -> None: + request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = self.client._build_request( + FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) + ) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(100.0) + + def test_client_timeout_option(self) -> None: + client = CasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(0) + + def test_http_client_timeout_option(self) -> None: + # custom timeout given to the httpx client should be used + with httpx.Client(timeout=None) as http_client: + client = CasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(None) + + # no timeout given to the httpx client should not use the httpx default + with httpx.Client() as http_client: + client = CasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + # explicitly passing the default timeout currently results in it being ignored + with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = CasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT # our default + + async def test_invalid_http_client(self) -> None: + with pytest.raises(TypeError, match="Invalid `http_client` arg"): + async with httpx.AsyncClient() as http_client: + CasParser( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + def test_default_headers_option(self) -> None: + client = CasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + client2 = CasParser( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "stainless" + assert request.headers.get("x-stainless-lang") == "my-overriding-header" + + def test_validate_headers(self) -> None: + client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-api-key") == api_key + + with pytest.raises(CasParserError): + with update_env(**{"CAS_PARSER_API_KEY": Omit()}): + client2 = CasParser(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 + + def test_default_query_option(self) -> None: + client = CasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + assert dict(url.params) == {"query_param": "bar"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo", + params={"foo": "baz", "query_param": "overridden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} + + def test_request_extra_json(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": False} + + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"baz": False} + + # `extra_json` takes priority over `json_data` when keys clash + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar", "baz": True}, + extra_json={"baz": None}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": None} + + def test_request_extra_headers(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"X-Foo": "Foo"}), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + + # `extra_headers` takes priority over `default_headers` when keys clash + request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_headers={"X-Bar": "false"}, + ), + ), + ) + assert request.headers.get("X-Bar") == "false" + + def test_request_extra_query(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_query={"my_query_param": "Foo"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"my_query_param": "Foo"} + + # if both `query` and `extra_query` are given, they are merged + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"bar": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"bar": "1", "foo": "2"} + + # `extra_query` takes priority over `query` when keys clash + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"foo": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"foo": "2"} + + def test_multipart_repeating_array(self, client: CasParser) -> None: + request = client._build_request( + FinalRequestOptions.construct( + method="post", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + def test_basic_union_response(self, respx_mock: MockRouter) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = self.client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + def test_base_url_setter(self) -> None: + client = CasParser(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True) + assert client.base_url == "https://example.com/from_init/" + + client.base_url = "https://example.com/from_setter" # type: ignore[assignment] + + assert client.base_url == "https://example.com/from_setter/" + + def test_base_url_env(self) -> None: + with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): + client = CasParser(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + # explicit environment arg requires explicitness + with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + CasParser(api_key=api_key, _strict_response_validation=True, environment="production") + + client = CasParser( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://portfolio-parser.api.casparser.in") + + @pytest.mark.parametrize( + "client", + [ + CasParser(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + CasParser( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_trailing_slash(self, client: CasParser) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + CasParser(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + CasParser( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_no_trailing_slash(self, client: CasParser) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + CasParser(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), + CasParser( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_absolute_request_url(self, client: CasParser) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + + def test_copied_client_does_not_close_http(self) -> None: + client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not client.is_closed() + + copied = client.copy() + assert copied is not client + + del copied + + assert not client.is_closed() + + def test_client_context_manager(self) -> None: + client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + with client as c2: + assert c2 is client + assert not c2.is_closed() + assert not client.is_closed() + assert client.is_closed() + + @pytest.mark.respx(base_url=base_url) + def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) + + with pytest.raises(APIResponseValidationError) as exc: + self.client.get("/foo", cast_to=Model) + + assert isinstance(exc.value.__cause__, ValidationError) + + def test_client_max_retries_validation(self) -> None: + with pytest.raises(TypeError, match=r"max_retries cannot be None"): + CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) + + @pytest.mark.respx(base_url=base_url) + def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + strict_client.get("/foo", cast_to=Model) + + client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 0.5], + [3, "-10", 0.5], + [3, "60", 60], + [3, "61", 0.5], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], + [-1100, "", 8], # test large number potentially overflowing + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: + client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + headers = httpx.Headers({"retry-after": retry_after}) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) + calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + + @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: CasParser) -> None: + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + client.cas_parser.with_streaming_response.cams_kfintech().__enter__() + + assert _get_open_connections(self.client) == 0 + + @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: CasParser) -> None: + respx_mock.post("/v4/cams_kfintech/parse").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + client.cas_parser.with_streaming_response.cams_kfintech().__enter__() + assert _get_open_connections(self.client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) + def test_retries_taken( + self, + client: CasParser, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + + response = client.cas_parser.with_raw_response.cams_kfintech() + + assert response.retries_taken == failures_before_success + assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_omit_retry_count_header( + self, client: CasParser, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + + response = client.cas_parser.with_raw_response.cams_kfintech(extra_headers={"x-stainless-retry-count": Omit()}) + + assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_overwrite_retry_count_header( + self, client: CasParser, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + + response = client.cas_parser.with_raw_response.cams_kfintech(extra_headers={"x-stainless-retry-count": "42"}) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" + + +class TestAsyncCasParser: + client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_raw_response(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await self.client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = await self.client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + def test_copy(self) -> None: + copied = self.client.copy() + assert id(copied) != id(self.client) + + copied = self.client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert self.client.api_key == "My API Key" + + def test_copy_default_options(self) -> None: + # options that have a default are overridden correctly + copied = self.client.copy(max_retries=7) + assert copied.max_retries == 7 + assert self.client.max_retries == 2 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(self.client.timeout, httpx.Timeout) + copied = self.client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(self.client.timeout, httpx.Timeout) + + def test_copy_default_headers(self) -> None: + client = AsyncCasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + assert client.default_headers["X-Foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert copied.default_headers["X-Foo"] == "bar" + + # merges already given headers + copied = client.copy(default_headers={"X-Bar": "stainless"}) + assert copied.default_headers["X-Foo"] == "bar" + assert copied.default_headers["X-Bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_headers={"X-Foo": "stainless"}) + assert copied.default_headers["X-Foo"] == "stainless" + + # set_default_headers + + # completely overrides already set values + copied = client.copy(set_default_headers={}) + assert copied.default_headers.get("X-Foo") is None + + copied = client.copy(set_default_headers={"X-Bar": "Robert"}) + assert copied.default_headers["X-Bar"] == "Robert" + + with pytest.raises( + ValueError, + match="`default_headers` and `set_default_headers` arguments are mutually exclusive", + ): + client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + + def test_copy_default_query(self) -> None: + client = AsyncCasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + def test_copy_signature(self) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + self.client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(self.client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") + def test_copy_build_request(self) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client = self.client.copy() + client._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "cas_parser/_legacy_response.py", + "cas_parser/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "cas_parser/_compat.py", + # Standard library leaks we don't care about. + "/logging/__init__.py", + ] + ): + return + + leaks.append(diff) + + leaks: list[tracemalloc.StatisticDiff] = [] + for diff in snapshot_after.compare_to(snapshot_before, "traceback"): + add_leak(leaks, diff) + if leaks: + for leak in leaks: + print("MEMORY LEAK:", leak) + for frame in leak.traceback: + print(frame) + raise AssertionError() + + async def test_request_timeout(self) -> None: + request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = self.client._build_request( + FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) + ) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(100.0) + + async def test_client_timeout_option(self) -> None: + client = AsyncCasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(0) + + async def test_http_client_timeout_option(self) -> None: + # custom timeout given to the httpx client should be used + async with httpx.AsyncClient(timeout=None) as http_client: + client = AsyncCasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(None) + + # no timeout given to the httpx client should not use the httpx default + async with httpx.AsyncClient() as http_client: + client = AsyncCasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + # explicitly passing the default timeout currently results in it being ignored + async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = AsyncCasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT # our default + + def test_invalid_http_client(self) -> None: + with pytest.raises(TypeError, match="Invalid `http_client` arg"): + with httpx.Client() as http_client: + AsyncCasParser( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + def test_default_headers_option(self) -> None: + client = AsyncCasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + client2 = AsyncCasParser( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "stainless" + assert request.headers.get("x-stainless-lang") == "my-overriding-header" + + def test_validate_headers(self) -> None: + client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-api-key") == api_key + + with pytest.raises(CasParserError): + with update_env(**{"CAS_PARSER_API_KEY": Omit()}): + client2 = AsyncCasParser(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 + + def test_default_query_option(self) -> None: + client = AsyncCasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + assert dict(url.params) == {"query_param": "bar"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo", + params={"foo": "baz", "query_param": "overridden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} + + def test_request_extra_json(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": False} + + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"baz": False} + + # `extra_json` takes priority over `json_data` when keys clash + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar", "baz": True}, + extra_json={"baz": None}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": None} + + def test_request_extra_headers(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"X-Foo": "Foo"}), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + + # `extra_headers` takes priority over `default_headers` when keys clash + request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_headers={"X-Bar": "false"}, + ), + ), + ) + assert request.headers.get("X-Bar") == "false" + + def test_request_extra_query(self) -> None: + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_query={"my_query_param": "Foo"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"my_query_param": "Foo"} + + # if both `query` and `extra_query` are given, they are merged + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"bar": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"bar": "1", "foo": "2"} + + # `extra_query` takes priority over `query` when keys clash + request = self.client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"foo": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"foo": "2"} + + def test_multipart_repeating_array(self, async_client: AsyncCasParser) -> None: + request = async_client._build_request( + FinalRequestOptions.construct( + method="post", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + async def test_basic_union_response(self, respx_mock: MockRouter) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + async def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = await self.client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + def test_base_url_setter(self) -> None: + client = AsyncCasParser( + base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True + ) + assert client.base_url == "https://example.com/from_init/" + + client.base_url = "https://example.com/from_setter" # type: ignore[assignment] + + assert client.base_url == "https://example.com/from_setter/" + + def test_base_url_env(self) -> None: + with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): + client = AsyncCasParser(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + # explicit environment arg requires explicitness + with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + AsyncCasParser(api_key=api_key, _strict_response_validation=True, environment="production") + + client = AsyncCasParser( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://portfolio-parser.api.casparser.in") + + @pytest.mark.parametrize( + "client", + [ + AsyncCasParser( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncCasParser( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_trailing_slash(self, client: AsyncCasParser) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + AsyncCasParser( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncCasParser( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_no_trailing_slash(self, client: AsyncCasParser) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + + @pytest.mark.parametrize( + "client", + [ + AsyncCasParser( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncCasParser( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_absolute_request_url(self, client: AsyncCasParser) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + + async def test_copied_client_does_not_close_http(self) -> None: + client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not client.is_closed() + + copied = client.copy() + assert copied is not client + + del copied + + await asyncio.sleep(0.2) + assert not client.is_closed() + + async def test_client_context_manager(self) -> None: + client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + async with client as c2: + assert c2 is client + assert not c2.is_closed() + assert not client.is_closed() + assert client.is_closed() + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) + + with pytest.raises(APIResponseValidationError) as exc: + await self.client.get("/foo", cast_to=Model) + + assert isinstance(exc.value.__cause__, ValidationError) + + async def test_client_max_retries_validation(self) -> None: + with pytest.raises(TypeError, match=r"max_retries cannot be None"): + AsyncCasParser( + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) + ) + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + await strict_client.get("/foo", cast_to=Model) + + client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = await client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 0.5], + [3, "-10", 0.5], + [3, "60", 60], + [3, "61", 0.5], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], + [-1100, "", 8], # test large number potentially overflowing + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + @pytest.mark.asyncio + async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: + client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + headers = httpx.Headers({"retry-after": retry_after}) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) + calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + + @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_timeout_errors_doesnt_leak( + self, respx_mock: MockRouter, async_client: AsyncCasParser + ) -> None: + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + await async_client.cas_parser.with_streaming_response.cams_kfintech().__aenter__() + + assert _get_open_connections(self.client) == 0 + + @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_status_errors_doesnt_leak( + self, respx_mock: MockRouter, async_client: AsyncCasParser + ) -> None: + respx_mock.post("/v4/cams_kfintech/parse").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + await async_client.cas_parser.with_streaming_response.cams_kfintech().__aenter__() + assert _get_open_connections(self.client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) + async def test_retries_taken( + self, + async_client: AsyncCasParser, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + + response = await client.cas_parser.with_raw_response.cams_kfintech() + + assert response.retries_taken == failures_before_success + assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_omit_retry_count_header( + self, async_client: AsyncCasParser, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + + response = await client.cas_parser.with_raw_response.cams_kfintech( + extra_headers={"x-stainless-retry-count": Omit()} + ) + + assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_overwrite_retry_count_header( + self, async_client: AsyncCasParser, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + + response = await client.cas_parser.with_raw_response.cams_kfintech( + extra_headers={"x-stainless-retry-count": "42"} + ) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + def test_get_platform(self) -> None: + # A previous implementation of asyncify could leave threads unterminated when + # used with nest_asyncio. + # + # Since nest_asyncio.apply() is global and cannot be un-applied, this + # test is run in a separate process to avoid affecting other tests. + test_code = dedent(""" + import asyncio + import nest_asyncio + import threading + + from cas_parser._utils import asyncify + from cas_parser._base_client import get_platform + + async def test_main() -> None: + result = await asyncify(get_platform)() + print(result) + for thread in threading.enumerate(): + print(thread.name) + + nest_asyncio.apply() + asyncio.run(test_main()) + """) + with subprocess.Popen( + [sys.executable, "-c", test_code], + text=True, + ) as process: + timeout = 10 # seconds + + start_time = time.monotonic() + while True: + return_code = process.poll() + if return_code is not None: + if return_code != 0: + raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") + + # success + break + + if time.monotonic() - start_time > timeout: + process.kill() + raise AssertionError("calling get_platform using asyncify resulted in a hung process") + + time.sleep(0.1) + + async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + + client = DefaultAsyncHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + async def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultAsyncHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + await self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py new file mode 100644 index 0000000..c1e03c0 --- /dev/null +++ b/tests/test_deepcopy.py @@ -0,0 +1,58 @@ +from cas_parser._utils import deepcopy_minimal + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert id(obj1) != id(obj2) + + +def test_simple_dict() -> None: + obj1 = {"foo": "bar"} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + + +def test_nested_dict() -> None: + obj1 = {"foo": {"bar": True}} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1["foo"], obj2["foo"]) + + +def test_complex_nested_dict() -> None: + obj1 = {"foo": {"bar": [{"hello": "world"}]}} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1["foo"], obj2["foo"]) + assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) + assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) + + +def test_simple_list() -> None: + obj1 = ["a", "b", "c"] + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + + +def test_nested_list() -> None: + obj1 = ["a", [1, 2, 3]] + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert_different_identities(obj1[1], obj2[1]) + + +class MyObject: ... + + +def test_ignores_other_types() -> None: + # custom classes + my_obj = MyObject() + obj1 = {"foo": my_obj} + obj2 = deepcopy_minimal(obj1) + assert_different_identities(obj1, obj2) + assert obj1["foo"] is my_obj + + # tuples + obj3 = ("a", "b") + obj4 = deepcopy_minimal(obj3) + assert obj3 is obj4 diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py new file mode 100644 index 0000000..37985fb --- /dev/null +++ b/tests/test_extract_files.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from typing import Sequence + +import pytest + +from cas_parser._types import FileTypes +from cas_parser._utils import extract_files + + +def test_removes_files_from_input() -> None: + query = {"foo": "bar"} + assert extract_files(query, paths=[]) == [] + assert query == {"foo": "bar"} + + query2 = {"foo": b"Bar", "hello": "world"} + assert extract_files(query2, paths=[["foo"]]) == [("foo", b"Bar")] + assert query2 == {"hello": "world"} + + query3 = {"foo": {"foo": {"bar": b"Bar"}}, "hello": "world"} + assert extract_files(query3, paths=[["foo", "foo", "bar"]]) == [("foo[foo][bar]", b"Bar")] + assert query3 == {"foo": {"foo": {}}, "hello": "world"} + + query4 = {"foo": {"bar": b"Bar", "baz": "foo"}, "hello": "world"} + assert extract_files(query4, paths=[["foo", "bar"]]) == [("foo[bar]", b"Bar")] + assert query4 == {"hello": "world", "foo": {"baz": "foo"}} + + +def test_multiple_files() -> None: + query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]} + assert extract_files(query, paths=[["documents", "", "file"]]) == [ + ("documents[][file]", b"My first file"), + ("documents[][file]", b"My second file"), + ] + assert query == {"documents": [{}, {}]} + + +@pytest.mark.parametrize( + "query,paths,expected", + [ + [ + {"foo": {"bar": "baz"}}, + [["foo", "", "bar"]], + [], + ], + [ + {"foo": ["bar", "baz"]}, + [["foo", "bar"]], + [], + ], + [ + {"foo": {"bar": "baz"}}, + [["foo", "foo"]], + [], + ], + ], + ids=["dict expecting array", "array expecting dict", "unknown keys"], +) +def test_ignores_incorrect_paths( + query: dict[str, object], + paths: Sequence[Sequence[str]], + expected: list[tuple[str, FileTypes]], +) -> None: + assert extract_files(query, paths=paths) == expected diff --git a/tests/test_files.py b/tests/test_files.py new file mode 100644 index 0000000..1f448b8 --- /dev/null +++ b/tests/test_files.py @@ -0,0 +1,51 @@ +from pathlib import Path + +import anyio +import pytest +from dirty_equals import IsDict, IsList, IsBytes, IsTuple + +from cas_parser._files import to_httpx_files, async_to_httpx_files + +readme_path = Path(__file__).parent.parent.joinpath("README.md") + + +def test_pathlib_includes_file_name() -> None: + result = to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +def test_tuple_input() -> None: + result = to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +@pytest.mark.asyncio +async def test_async_pathlib_includes_file_name() -> None: + result = await async_to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_supports_anyio_path() -> None: + result = await async_to_httpx_files({"file": anyio.Path(readme_path)}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_tuple_input() -> None: + result = await async_to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +def test_string_not_allowed() -> None: + with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): + to_httpx_files( + { + "file": "foo", # type: ignore + } + ) diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..2a79cf8 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,963 @@ +import json +from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast +from datetime import datetime, timezone +from typing_extensions import Literal, Annotated, TypeAliasType + +import pytest +import pydantic +from pydantic import Field + +from cas_parser._utils import PropertyInfo +from cas_parser._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from cas_parser._models import BaseModel, construct_type + + +class BasicModel(BaseModel): + foo: str + + +@pytest.mark.parametrize("value", ["hello", 1], ids=["correct type", "mismatched"]) +def test_basic(value: object) -> None: + m = BasicModel.construct(foo=value) + assert m.foo == value + + +def test_directly_nested_model() -> None: + class NestedModel(BaseModel): + nested: BasicModel + + m = NestedModel.construct(nested={"foo": "Foo!"}) + assert m.nested.foo == "Foo!" + + # mismatched types + m = NestedModel.construct(nested="hello!") + assert cast(Any, m.nested) == "hello!" + + +def test_optional_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[BasicModel] + + m1 = NestedModel.construct(nested=None) + assert m1.nested is None + + m2 = NestedModel.construct(nested={"foo": "bar"}) + assert m2.nested is not None + assert m2.nested.foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested={"foo"}) + assert isinstance(cast(Any, m3.nested), set) + assert cast(Any, m3.nested) == {"foo"} + + +def test_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[BasicModel] + + m = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0].foo == "bar" + assert m.nested[1].foo == "2" + + # mismatched types + m = NestedModel.construct(nested=True) + assert cast(Any, m.nested) is True + + m = NestedModel.construct(nested=[False]) + assert cast(Any, m.nested) == [False] + + +def test_optional_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[List[BasicModel]] + + m1 = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m1.nested is not None + assert isinstance(m1.nested, list) + assert len(m1.nested) == 2 + assert m1.nested[0].foo == "bar" + assert m1.nested[1].foo == "2" + + m2 = NestedModel.construct(nested=None) + assert m2.nested is None + + # mismatched types + m3 = NestedModel.construct(nested={1}) + assert cast(Any, m3.nested) == {1} + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_optional_items_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[Optional[BasicModel]] + + m = NestedModel.construct(nested=[None, {"foo": "bar"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0] is None + assert m.nested[1] is not None + assert m.nested[1].foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested="foo") + assert cast(Any, m3.nested) == "foo" + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_mismatched_type() -> None: + class NestedModel(BaseModel): + nested: List[str] + + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_raw_dictionary() -> None: + class NestedModel(BaseModel): + nested: Dict[str, str] + + m = NestedModel.construct(nested={"hello": "world"}) + assert m.nested == {"hello": "world"} + + # mismatched types + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_nested_dictionary_model() -> None: + class NestedModel(BaseModel): + nested: Dict[str, BasicModel] + + m = NestedModel.construct(nested={"hello": {"foo": "bar"}}) + assert isinstance(m.nested, dict) + assert m.nested["hello"].foo == "bar" + + # mismatched types + m = NestedModel.construct(nested={"hello": False}) + assert cast(Any, m.nested["hello"]) is False + + +def test_unknown_fields() -> None: + m1 = BasicModel.construct(foo="foo", unknown=1) + assert m1.foo == "foo" + assert cast(Any, m1).unknown == 1 + + m2 = BasicModel.construct(foo="foo", unknown={"foo_bar": True}) + assert m2.foo == "foo" + assert cast(Any, m2).unknown == {"foo_bar": True} + + assert model_dump(m2) == {"foo": "foo", "unknown": {"foo_bar": True}} + + +def test_strict_validation_unknown_fields() -> None: + class Model(BaseModel): + foo: str + + model = parse_obj(Model, dict(foo="hello!", user="Robert")) + assert model.foo == "hello!" + assert cast(Any, model).user == "Robert" + + assert model_dump(model) == {"foo": "hello!", "user": "Robert"} + + +def test_aliases() -> None: + class Model(BaseModel): + my_field: int = Field(alias="myField") + + m = Model.construct(myField=1) + assert m.my_field == 1 + + # mismatched types + m = Model.construct(myField={"hello": False}) + assert cast(Any, m.my_field) == {"hello": False} + + +def test_repr() -> None: + model = BasicModel(foo="bar") + assert str(model) == "BasicModel(foo='bar')" + assert repr(model) == "BasicModel(foo='bar')" + + +def test_repr_nested_model() -> None: + class Child(BaseModel): + name: str + age: int + + class Parent(BaseModel): + name: str + child: Child + + model = Parent(name="Robert", child=Child(name="Foo", age=5)) + assert str(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + assert repr(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + + +def test_optional_list() -> None: + class Submodel(BaseModel): + name: str + + class Model(BaseModel): + items: Optional[List[Submodel]] + + m = Model.construct(items=None) + assert m.items is None + + m = Model.construct(items=[]) + assert m.items == [] + + m = Model.construct(items=[{"name": "Robert"}]) + assert m.items is not None + assert len(m.items) == 1 + assert m.items[0].name == "Robert" + + +def test_nested_union_of_models() -> None: + class Submodel1(BaseModel): + bar: bool + + class Submodel2(BaseModel): + thing: str + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2] + + m = Model.construct(foo={"thing": "hello"}) + assert isinstance(m.foo, Submodel2) + assert m.foo.thing == "hello" + + +def test_nested_union_of_mixed_types() -> None: + class Submodel1(BaseModel): + bar: bool + + class Model(BaseModel): + foo: Union[Submodel1, Literal[True], Literal["CARD_HOLDER"]] + + m = Model.construct(foo=True) + assert m.foo is True + + m = Model.construct(foo="CARD_HOLDER") + assert m.foo == "CARD_HOLDER" + + m = Model.construct(foo={"bar": False}) + assert isinstance(m.foo, Submodel1) + assert m.foo.bar is False + + +def test_nested_union_multiple_variants() -> None: + class Submodel1(BaseModel): + bar: bool + + class Submodel2(BaseModel): + thing: str + + class Submodel3(BaseModel): + foo: int + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2, None, Submodel3] + + m = Model.construct(foo={"thing": "hello"}) + assert isinstance(m.foo, Submodel2) + assert m.foo.thing == "hello" + + m = Model.construct(foo=None) + assert m.foo is None + + m = Model.construct() + assert m.foo is None + + m = Model.construct(foo={"foo": "1"}) + assert isinstance(m.foo, Submodel3) + assert m.foo.foo == 1 + + +def test_nested_union_invalid_data() -> None: + class Submodel1(BaseModel): + level: int + + class Submodel2(BaseModel): + name: str + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2] + + m = Model.construct(foo=True) + assert cast(bool, m.foo) is True + + m = Model.construct(foo={"name": 3}) + if PYDANTIC_V2: + assert isinstance(m.foo, Submodel1) + assert m.foo.name == 3 # type: ignore + else: + assert isinstance(m.foo, Submodel2) + assert m.foo.name == "3" + + +def test_list_of_unions() -> None: + class Submodel1(BaseModel): + level: int + + class Submodel2(BaseModel): + name: str + + class Model(BaseModel): + items: List[Union[Submodel1, Submodel2]] + + m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) + assert len(m.items) == 2 + assert isinstance(m.items[0], Submodel1) + assert m.items[0].level == 1 + assert isinstance(m.items[1], Submodel2) + assert m.items[1].name == "Robert" + + m = Model.construct(items=[{"level": -1}, 156]) + assert len(m.items) == 2 + assert isinstance(m.items[0], Submodel1) + assert m.items[0].level == -1 + assert cast(Any, m.items[1]) == 156 + + +def test_union_of_lists() -> None: + class SubModel1(BaseModel): + level: int + + class SubModel2(BaseModel): + name: str + + class Model(BaseModel): + items: Union[List[SubModel1], List[SubModel2]] + + # with one valid entry + m = Model.construct(items=[{"name": "Robert"}]) + assert len(m.items) == 1 + assert isinstance(m.items[0], SubModel2) + assert m.items[0].name == "Robert" + + # with two entries pointing to different types + m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == 1 + assert isinstance(m.items[1], SubModel1) + assert cast(Any, m.items[1]).name == "Robert" + + # with two entries pointing to *completely* different types + m = Model.construct(items=[{"level": -1}, 156]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == -1 + assert cast(Any, m.items[1]) == 156 + + +def test_dict_of_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Dict[str, Union[SubModel1, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel2) + assert m.data["foo"].foo == "bar" + + # TODO: test mismatched type + + +def test_double_nested_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + bar: str + + class Model(BaseModel): + data: Dict[str, List[Union[SubModel1, SubModel2]]] + + m = Model.construct(data={"foo": [{"bar": "baz"}, {"name": "Robert"}]}) + assert len(m.data["foo"]) == 2 + + entry1 = m.data["foo"][0] + assert isinstance(entry1, SubModel2) + assert entry1.bar == "baz" + + entry2 = m.data["foo"][1] + assert isinstance(entry2, SubModel1) + assert entry2.name == "Robert" + + # TODO: test mismatched type + + +def test_union_of_dict() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Union[Dict[str, SubModel1], Dict[str, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel1) + assert cast(Any, m.data["foo"]).foo == "bar" + + +def test_iso8601_datetime() -> None: + class Model(BaseModel): + created_at: datetime + + expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) + + if PYDANTIC_V2: + expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' + else: + expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' + + model = Model.construct(created_at="2019-12-27T18:11:19.117Z") + assert model.created_at == expected + assert model_json(model) == expected_json + + model = parse_obj(Model, dict(created_at="2019-12-27T18:11:19.117Z")) + assert model.created_at == expected + assert model_json(model) == expected_json + + +def test_does_not_coerce_int() -> None: + class Model(BaseModel): + bar: int + + assert Model.construct(bar=1).bar == 1 + assert Model.construct(bar=10.9).bar == 10.9 + assert Model.construct(bar="19").bar == "19" # type: ignore[comparison-overlap] + assert Model.construct(bar=False).bar is False + + +def test_int_to_float_safe_conversion() -> None: + class Model(BaseModel): + float_field: float + + m = Model.construct(float_field=10) + assert m.float_field == 10.0 + assert isinstance(m.float_field, float) + + m = Model.construct(float_field=10.12) + assert m.float_field == 10.12 + assert isinstance(m.float_field, float) + + # number too big + m = Model.construct(float_field=2**53 + 1) + assert m.float_field == 2**53 + 1 + assert isinstance(m.float_field, int) + + +def test_deprecated_alias() -> None: + class Model(BaseModel): + resource_id: str = Field(alias="model_id") + + @property + def model_id(self) -> str: + return self.resource_id + + m = Model.construct(model_id="id") + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + m = parse_obj(Model, {"model_id": "id"}) + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + +def test_omitted_fields() -> None: + class Model(BaseModel): + resource_id: Optional[str] = None + + m = Model.construct() + assert m.resource_id is None + assert "resource_id" not in m.model_fields_set + + m = Model.construct(resource_id=None) + assert m.resource_id is None + assert "resource_id" in m.model_fields_set + + m = Model.construct(resource_id="foo") + assert m.resource_id == "foo" + assert "resource_id" in m.model_fields_set + + +def test_to_dict() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert m.to_dict() == {"FOO": "hello"} + assert m.to_dict(use_api_names=False) == {"foo": "hello"} + + m2 = Model() + assert m2.to_dict() == {} + assert m2.to_dict(exclude_unset=False) == {"FOO": None} + assert m2.to_dict(exclude_unset=False, exclude_none=True) == {} + assert m2.to_dict(exclude_unset=False, exclude_defaults=True) == {} + + m3 = Model(FOO=None) + assert m3.to_dict() == {"FOO": None} + assert m3.to_dict(exclude_none=True) == {} + assert m3.to_dict(exclude_defaults=True) == {} + + class Model2(BaseModel): + created_at: datetime + + time_str = "2024-03-21T11:39:01.275859" + m4 = Model2.construct(created_at=time_str) + assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} + assert m4.to_dict(mode="json") == {"created_at": time_str} + + if not PYDANTIC_V2: + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_dict(warnings=False) + + +def test_forwards_compat_model_dump_method() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert m.model_dump() == {"foo": "hello"} + assert m.model_dump(include={"bar"}) == {} + assert m.model_dump(exclude={"foo"}) == {} + assert m.model_dump(by_alias=True) == {"FOO": "hello"} + + m2 = Model() + assert m2.model_dump() == {"foo": None} + assert m2.model_dump(exclude_unset=True) == {} + assert m2.model_dump(exclude_none=True) == {} + assert m2.model_dump(exclude_defaults=True) == {} + + m3 = Model(FOO=None) + assert m3.model_dump() == {"foo": None} + assert m3.model_dump(exclude_none=True) == {} + + if not PYDANTIC_V2: + with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): + m.model_dump(round_trip=True) + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.model_dump(warnings=False) + + +def test_compat_method_no_error_for_warnings() -> None: + class Model(BaseModel): + foo: Optional[str] + + m = Model(foo="hello") + assert isinstance(model_dump(m, warnings=False), dict) + + +def test_to_json() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert json.loads(m.to_json()) == {"FOO": "hello"} + assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} + + if PYDANTIC_V2: + assert m.to_json(indent=None) == '{"FOO":"hello"}' + else: + assert m.to_json(indent=None) == '{"FOO": "hello"}' + + m2 = Model() + assert json.loads(m2.to_json()) == {} + assert json.loads(m2.to_json(exclude_unset=False)) == {"FOO": None} + assert json.loads(m2.to_json(exclude_unset=False, exclude_none=True)) == {} + assert json.loads(m2.to_json(exclude_unset=False, exclude_defaults=True)) == {} + + m3 = Model(FOO=None) + assert json.loads(m3.to_json()) == {"FOO": None} + assert json.loads(m3.to_json(exclude_none=True)) == {} + + if not PYDANTIC_V2: + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_json(warnings=False) + + +def test_forwards_compat_model_dump_json_method() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert json.loads(m.model_dump_json()) == {"foo": "hello"} + assert json.loads(m.model_dump_json(include={"bar"})) == {} + assert json.loads(m.model_dump_json(include={"foo"})) == {"foo": "hello"} + assert json.loads(m.model_dump_json(by_alias=True)) == {"FOO": "hello"} + + assert m.model_dump_json(indent=2) == '{\n "foo": "hello"\n}' + + m2 = Model() + assert json.loads(m2.model_dump_json()) == {"foo": None} + assert json.loads(m2.model_dump_json(exclude_unset=True)) == {} + assert json.loads(m2.model_dump_json(exclude_none=True)) == {} + assert json.loads(m2.model_dump_json(exclude_defaults=True)) == {} + + m3 = Model(FOO=None) + assert json.loads(m3.model_dump_json()) == {"foo": None} + assert json.loads(m3.model_dump_json(exclude_none=True)) == {} + + if not PYDANTIC_V2: + with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): + m.model_dump_json(round_trip=True) + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.model_dump_json(warnings=False) + + +def test_type_compat() -> None: + # our model type can be assigned to Pydantic's model type + + def takes_pydantic(model: pydantic.BaseModel) -> None: # noqa: ARG001 + ... + + class OurModel(BaseModel): + foo: Optional[str] = None + + takes_pydantic(OurModel()) + + +def test_annotated_types() -> None: + class Model(BaseModel): + value: str + + m = construct_type( + value={"value": "foo"}, + type_=cast(Any, Annotated[Model, "random metadata"]), + ) + assert isinstance(m, Model) + assert m.value == "foo" + + +def test_discriminated_unions_invalid_data() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "a", "data": 100}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, A) + assert m.type == "a" + if PYDANTIC_V2: + assert m.data == 100 # type: ignore[comparison-overlap] + else: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + + +def test_discriminated_unions_unknown_variant() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + m = construct_type( + value={"type": "c", "data": None, "new_thing": "bar"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + + # just chooses the first variant + assert isinstance(m, A) + assert m.type == "c" # type: ignore[comparison-overlap] + assert m.data == None # type: ignore[unreachable] + assert m.new_thing == "bar" + + +def test_discriminated_unions_invalid_data_nested_unions() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + class C(BaseModel): + type: Literal["c"] + + data: bool + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "c", "data": "foo"}, + type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, C) + assert m.type == "c" + assert m.data == "foo" # type: ignore[comparison-overlap] + + +def test_discriminated_unions_with_aliases_invalid_data() -> None: + class A(BaseModel): + foo_type: Literal["a"] = Field(alias="type") + + data: str + + class B(BaseModel): + foo_type: Literal["b"] = Field(alias="type") + + data: int + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), + ) + assert isinstance(m, B) + assert m.foo_type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "a", "data": 100}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), + ) + assert isinstance(m, A) + assert m.foo_type == "a" + if PYDANTIC_V2: + assert m.data == 100 # type: ignore[comparison-overlap] + else: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + + +def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["a"] + + data: int + + m = construct_type( + value={"type": "a", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "a" + assert m.data == "foo" # type: ignore[comparison-overlap] + + +def test_discriminated_unions_invalid_data_uses_cache() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + UnionType = cast(Any, Union[A, B]) + + assert not hasattr(UnionType, "__discriminator__") + + m = construct_type( + value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + discriminator = UnionType.__discriminator__ + assert discriminator is not None + + m = construct_type( + value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + # if the discriminator details object stays the same between invocations then + # we hit the cache + assert UnionType.__discriminator__ is discriminator + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +def test_type_alias_type() -> None: + Alias = TypeAliasType("Alias", str) # pyright: ignore + + class Model(BaseModel): + alias: Alias + union: Union[int, Alias] + + m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.alias, str) + assert m.alias == "foo" + assert isinstance(m.union, str) + assert m.union == "bar" + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +def test_field_named_cls() -> None: + class Model(BaseModel): + cls: str + + m = construct_type(value={"cls": "foo"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.cls, str) + + +def test_discriminated_union_case() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["b"] + + data: List[Union[A, object]] + + class ModelA(BaseModel): + type: Literal["modelA"] + + data: int + + class ModelB(BaseModel): + type: Literal["modelB"] + + required: str + + data: Union[A, B] + + # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required` + m = construct_type( + value={"type": "modelB", "data": {"type": "a", "data": True}}, + type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]), + ) + + assert isinstance(m, ModelB) + + +def test_nested_discriminated_union() -> None: + class InnerType1(BaseModel): + type: Literal["type_1"] + + class InnerModel(BaseModel): + inner_value: str + + class InnerType2(BaseModel): + type: Literal["type_2"] + some_inner_model: InnerModel + + class Type1(BaseModel): + base_type: Literal["base_type_1"] + value: Annotated[ + Union[ + InnerType1, + InnerType2, + ], + PropertyInfo(discriminator="type"), + ] + + class Type2(BaseModel): + base_type: Literal["base_type_2"] + + T = Annotated[ + Union[ + Type1, + Type2, + ], + PropertyInfo(discriminator="base_type"), + ] + + model = construct_type( + type_=T, + value={ + "base_type": "base_type_1", + "value": { + "type": "type_2", + }, + }, + ) + assert isinstance(model, Type1) + assert isinstance(model.value, InnerType2) + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now") +def test_extra_properties() -> None: + class Item(BaseModel): + prop: int + + class Model(BaseModel): + __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + other: str + + if TYPE_CHECKING: + + def __getattr__(self, attr: str) -> Item: ... + + model = construct_type( + type_=Model, + value={ + "a": {"prop": 1}, + "other": "foo", + }, + ) + assert isinstance(model, Model) + assert model.a.prop == 1 + assert isinstance(model.a, Item) + assert model.other == "foo" diff --git a/tests/test_qs.py b/tests/test_qs.py new file mode 100644 index 0000000..0bc11a4 --- /dev/null +++ b/tests/test_qs.py @@ -0,0 +1,78 @@ +from typing import Any, cast +from functools import partial +from urllib.parse import unquote + +import pytest + +from cas_parser._qs import Querystring, stringify + + +def test_empty() -> None: + assert stringify({}) == "" + assert stringify({"a": {}}) == "" + assert stringify({"a": {"b": {"c": {}}}}) == "" + + +def test_basic() -> None: + assert stringify({"a": 1}) == "a=1" + assert stringify({"a": "b"}) == "a=b" + assert stringify({"a": True}) == "a=true" + assert stringify({"a": False}) == "a=false" + assert stringify({"a": 1.23456}) == "a=1.23456" + assert stringify({"a": None}) == "" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_nested_dotted(method: str) -> None: + if method == "class": + serialise = Querystring(nested_format="dots").stringify + else: + serialise = partial(stringify, nested_format="dots") + + assert unquote(serialise({"a": {"b": "c"}})) == "a.b=c" + assert unquote(serialise({"a": {"b": "c", "d": "e", "f": "g"}})) == "a.b=c&a.d=e&a.f=g" + assert unquote(serialise({"a": {"b": {"c": {"d": "e"}}}})) == "a.b.c.d=e" + assert unquote(serialise({"a": {"b": True}})) == "a.b=true" + + +def test_nested_brackets() -> None: + assert unquote(stringify({"a": {"b": "c"}})) == "a[b]=c" + assert unquote(stringify({"a": {"b": "c", "d": "e", "f": "g"}})) == "a[b]=c&a[d]=e&a[f]=g" + assert unquote(stringify({"a": {"b": {"c": {"d": "e"}}}})) == "a[b][c][d]=e" + assert unquote(stringify({"a": {"b": True}})) == "a[b]=true" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_comma(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="comma").stringify + else: + serialise = partial(stringify, array_format="comma") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in=foo,bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b]=true,false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b]=true,false,true" + + +def test_array_repeat() -> None: + assert unquote(stringify({"in": ["foo", "bar"]})) == "in=foo&in=bar" + assert unquote(stringify({"a": {"b": [True, False]}})) == "a[b]=true&a[b]=false" + assert unquote(stringify({"a": {"b": [True, False, None, True]}})) == "a[b]=true&a[b]=false&a[b]=true" + assert unquote(stringify({"in": ["foo", {"b": {"c": ["d", "e"]}}]})) == "in=foo&in[b][c]=d&in[b][c]=e" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_brackets(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="brackets").stringify + else: + serialise = partial(stringify, array_format="brackets") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in[]=foo&in[]=bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b][]=true&a[b][]=false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b][]=true&a[b][]=false&a[b][]=true" + + +def test_unknown_array_format() -> None: + with pytest.raises(NotImplementedError, match="Unknown array_format value: foo, choose from comma, repeat"): + stringify({"a": ["foo", "bar"]}, array_format=cast(Any, "foo")) diff --git a/tests/test_required_args.py b/tests/test_required_args.py new file mode 100644 index 0000000..deb79cd --- /dev/null +++ b/tests/test_required_args.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import pytest + +from cas_parser._utils import required_args + + +def test_too_many_positional_params() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + with pytest.raises(TypeError, match=r"foo\(\) takes 1 argument\(s\) but 2 were given"): + foo("a", "b") # type: ignore + + +def test_positional_param() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + assert foo("a") == "a" + assert foo(None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_keyword_only_param() -> None: + @required_args(["a"]) + def foo(*, a: str | None = None) -> str | None: + return a + + assert foo(a="a") == "a" + assert foo(a=None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_multiple_params() -> None: + @required_args(["a", "b", "c"]) + def foo(a: str = "", *, b: str = "", c: str = "") -> str | None: + return f"{a} {b} {c}" + + assert foo(a="a", b="b", c="c") == "a b c" + + error_message = r"Missing required arguments.*" + + with pytest.raises(TypeError, match=error_message): + foo() + + with pytest.raises(TypeError, match=error_message): + foo(a="a") + + with pytest.raises(TypeError, match=error_message): + foo(b="b") + + with pytest.raises(TypeError, match=error_message): + foo(c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'a'"): + foo(b="a", c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'b'"): + foo("a", c="c") + + +def test_multiple_variants() -> None: + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: str | None = None) -> str | None: + return a if a is not None else b + + assert foo(a="foo") == "foo" + assert foo(b="bar") == "bar" + assert foo(a=None) is None + assert foo(b=None) is None + + # TODO: this error message could probably be improved + with pytest.raises( + TypeError, + match=r"Missing required arguments; Expected either \('a'\) or \('b'\) arguments to be given", + ): + foo() + + +def test_multiple_params_multiple_variants() -> None: + @required_args(["a", "b"], ["c"]) + def foo(*, a: str | None = None, b: str | None = None, c: str | None = None) -> str | None: + if a is not None: + return a + if b is not None: + return b + return c + + error_message = r"Missing required arguments; Expected either \('a' and 'b'\) or \('c'\) arguments to be given" + + with pytest.raises(TypeError, match=error_message): + foo(a="foo") + + with pytest.raises(TypeError, match=error_message): + foo(b="bar") + + with pytest.raises(TypeError, match=error_message): + foo() + + assert foo(a=None, b="bar") == "bar" + assert foo(c=None) is None + assert foo(c="foo") == "foo" diff --git a/tests/test_response.py b/tests/test_response.py new file mode 100644 index 0000000..33023d5 --- /dev/null +++ b/tests/test_response.py @@ -0,0 +1,277 @@ +import json +from typing import Any, List, Union, cast +from typing_extensions import Annotated + +import httpx +import pytest +import pydantic + +from cas_parser import BaseModel, CasParser, AsyncCasParser +from cas_parser._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + BinaryAPIResponse, + AsyncBinaryAPIResponse, + extract_response_type, +) +from cas_parser._streaming import Stream +from cas_parser._base_client import FinalRequestOptions + + +class ConcreteBaseAPIResponse(APIResponse[bytes]): ... + + +class ConcreteAPIResponse(APIResponse[List[str]]): ... + + +class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): ... + + +def test_extract_response_type_direct_classes() -> None: + assert extract_response_type(BaseAPIResponse[str]) == str + assert extract_response_type(APIResponse[str]) == str + assert extract_response_type(AsyncAPIResponse[str]) == str + + +def test_extract_response_type_direct_class_missing_type_arg() -> None: + with pytest.raises( + RuntimeError, + match="Expected type to have a type argument at index 0 but it did not", + ): + extract_response_type(AsyncAPIResponse) + + +def test_extract_response_type_concrete_subclasses() -> None: + assert extract_response_type(ConcreteBaseAPIResponse) == bytes + assert extract_response_type(ConcreteAPIResponse) == List[str] + assert extract_response_type(ConcreteAsyncAPIResponse) == httpx.Response + + +def test_extract_response_type_binary_response() -> None: + assert extract_response_type(BinaryAPIResponse) == bytes + assert extract_response_type(AsyncBinaryAPIResponse) == bytes + + +class PydanticModel(pydantic.BaseModel): ... + + +def test_response_parse_mismatched_basemodel(client: CasParser) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo"), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + with pytest.raises( + TypeError, + match="Pydantic models must subclass our base model type, e.g. `from cas_parser import BaseModel`", + ): + response.parse(to=PydanticModel) + + +@pytest.mark.asyncio +async def test_async_response_parse_mismatched_basemodel(async_client: AsyncCasParser) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo"), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + with pytest.raises( + TypeError, + match="Pydantic models must subclass our base model type, e.g. `from cas_parser import BaseModel`", + ): + await response.parse(to=PydanticModel) + + +def test_response_parse_custom_stream(client: CasParser) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo"), + client=client, + stream=True, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + stream = response.parse(to=Stream[int]) + assert stream._cast_to == int + + +@pytest.mark.asyncio +async def test_async_response_parse_custom_stream(async_client: AsyncCasParser) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo"), + client=async_client, + stream=True, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + stream = await response.parse(to=Stream[int]) + assert stream._cast_to == int + + +class CustomModel(BaseModel): + foo: str + bar: int + + +def test_response_parse_custom_model(client: CasParser) -> None: + response = APIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse(to=CustomModel) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +@pytest.mark.asyncio +async def test_async_response_parse_custom_model(async_client: AsyncCasParser) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse(to=CustomModel) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +def test_response_parse_annotated_type(client: CasParser) -> None: + response = APIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse( + to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), + ) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +async def test_async_response_parse_annotated_type(async_client: AsyncCasParser) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse( + to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), + ) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +def test_response_parse_bool(client: CasParser, content: str, expected: bool) -> None: + response = APIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = response.parse(to=bool) + assert result is expected + + +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +async def test_async_response_parse_bool(client: AsyncCasParser, content: str, expected: bool) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = await response.parse(to=bool) + assert result is expected + + +class OtherModel(BaseModel): + a: str + + +@pytest.mark.parametrize("client", [False], indirect=True) # loose validation +def test_response_parse_expect_model_union_non_json_content(client: CasParser) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse(to=cast(Any, Union[CustomModel, OtherModel])) + assert isinstance(obj, str) + assert obj == "foo" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("async_client", [False], indirect=True) # loose validation +async def test_async_response_parse_expect_model_union_non_json_content(async_client: AsyncCasParser) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse(to=cast(Any, Union[CustomModel, OtherModel])) + assert isinstance(obj, str) + assert obj == "foo" diff --git a/tests/test_streaming.py b/tests/test_streaming.py new file mode 100644 index 0000000..5d9c369 --- /dev/null +++ b/tests/test_streaming.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +from typing import Iterator, AsyncIterator + +import httpx +import pytest + +from cas_parser import CasParser, AsyncCasParser +from cas_parser._streaming import Stream, AsyncStream, ServerSentEvent + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_basic(sync: bool, client: CasParser, async_client: AsyncCasParser) -> None: + def body() -> Iterator[bytes]: + yield b"event: completion\n" + yield b'data: {"foo":true}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_data_missing_event(sync: bool, client: CasParser, async_client: AsyncCasParser) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"foo":true}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_event_missing_data(sync: bool, client: CasParser, async_client: AsyncCasParser) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.data == "" + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_events(sync: bool, client: CasParser, async_client: AsyncCasParser) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"\n" + yield b"event: completion\n" + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.data == "" + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.data == "" + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_events_with_data(sync: bool, client: CasParser, async_client: AsyncCasParser) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b'data: {"foo":true}\n' + yield b"\n" + yield b"event: completion\n" + yield b'data: {"bar":false}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.json() == {"bar": False} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_data_lines_with_empty_line(sync: bool, client: CasParser, async_client: AsyncCasParser) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"data: {\n" + yield b'data: "foo":\n' + yield b"data: \n" + yield b"data:\n" + yield b"data: true}\n" + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + assert sse.data == '{\n"foo":\n\n\ntrue}' + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_data_json_escaped_double_new_line(sync: bool, client: CasParser, async_client: AsyncCasParser) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b'data: {"foo": "my long\\n\\ncontent"}' + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": "my long\n\ncontent"} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_data_lines(sync: bool, client: CasParser, async_client: AsyncCasParser) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"data: {\n" + yield b'data: "foo":\n' + yield b"data: true}\n" + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_special_new_line_character( + sync: bool, + client: CasParser, + async_client: AsyncCasParser, +) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"content":" culpa"}\n' + yield b"\n" + yield b'data: {"content":" \xe2\x80\xa8"}\n' + yield b"\n" + yield b'data: {"content":"foo"}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": " culpa"} + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": " 
"} + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": "foo"} + + await assert_empty_iter(iterator) + + +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multi_byte_character_multiple_chunks( + sync: bool, + client: CasParser, + async_client: AsyncCasParser, +) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"content":"' + # bytes taken from the string 'известни' and arbitrarily split + # so that some multi-byte characters span multiple chunks + yield b"\xd0" + yield b"\xb8\xd0\xb7\xd0" + yield b"\xb2\xd0\xb5\xd1\x81\xd1\x82\xd0\xbd\xd0\xb8" + yield b'"}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": "известни"} + + +async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: + for chunk in iter: + yield chunk + + +async def iter_next(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> ServerSentEvent: + if isinstance(iter, AsyncIterator): + return await iter.__anext__() + + return next(iter) + + +async def assert_empty_iter(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> None: + with pytest.raises((StopAsyncIteration, RuntimeError)): + await iter_next(iter) + + +def make_event_iterator( + content: Iterator[bytes], + *, + sync: bool, + client: CasParser, + async_client: AsyncCasParser, +) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: + if sync: + return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() + + return AsyncStream( + cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content)) + )._iter_events() diff --git a/tests/test_transform.py b/tests/test_transform.py new file mode 100644 index 0000000..5504d5e --- /dev/null +++ b/tests/test_transform.py @@ -0,0 +1,453 @@ +from __future__ import annotations + +import io +import pathlib +from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast +from datetime import date, datetime +from typing_extensions import Required, Annotated, TypedDict + +import pytest + +from cas_parser._types import NOT_GIVEN, Base64FileInput +from cas_parser._utils import ( + PropertyInfo, + transform as _transform, + parse_datetime, + async_transform as _async_transform, +) +from cas_parser._compat import PYDANTIC_V2 +from cas_parser._models import BaseModel + +_T = TypeVar("_T") + +SAMPLE_FILE_PATH = pathlib.Path(__file__).parent.joinpath("sample_file.txt") + + +async def transform( + data: _T, + expected_type: object, + use_async: bool, +) -> _T: + if use_async: + return await _async_transform(data, expected_type=expected_type) + + return _transform(data, expected_type=expected_type) + + +parametrize = pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"]) + + +class Foo1(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +@parametrize +@pytest.mark.asyncio +async def test_top_level_alias(use_async: bool) -> None: + assert await transform({"foo_bar": "hello"}, expected_type=Foo1, use_async=use_async) == {"fooBar": "hello"} + + +class Foo2(TypedDict): + bar: Bar2 + + +class Bar2(TypedDict): + this_thing: Annotated[int, PropertyInfo(alias="this__thing")] + baz: Annotated[Baz2, PropertyInfo(alias="Baz")] + + +class Baz2(TypedDict): + my_baz: Annotated[str, PropertyInfo(alias="myBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_recursive_typeddict(use_async: bool) -> None: + assert await transform({"bar": {"this_thing": 1}}, Foo2, use_async) == {"bar": {"this__thing": 1}} + assert await transform({"bar": {"baz": {"my_baz": "foo"}}}, Foo2, use_async) == {"bar": {"Baz": {"myBaz": "foo"}}} + + +class Foo3(TypedDict): + things: List[Bar3] + + +class Bar3(TypedDict): + my_field: Annotated[str, PropertyInfo(alias="myField")] + + +@parametrize +@pytest.mark.asyncio +async def test_list_of_typeddict(use_async: bool) -> None: + result = await transform({"things": [{"my_field": "foo"}, {"my_field": "foo2"}]}, Foo3, use_async) + assert result == {"things": [{"myField": "foo"}, {"myField": "foo2"}]} + + +class Foo4(TypedDict): + foo: Union[Bar4, Baz4] + + +class Bar4(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz4(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_union_of_typeddict(use_async: bool) -> None: + assert await transform({"foo": {"foo_bar": "bar"}}, Foo4, use_async) == {"foo": {"fooBar": "bar"}} + assert await transform({"foo": {"foo_baz": "baz"}}, Foo4, use_async) == {"foo": {"fooBaz": "baz"}} + assert await transform({"foo": {"foo_baz": "baz", "foo_bar": "bar"}}, Foo4, use_async) == { + "foo": {"fooBaz": "baz", "fooBar": "bar"} + } + + +class Foo5(TypedDict): + foo: Annotated[Union[Bar4, List[Baz4]], PropertyInfo(alias="FOO")] + + +class Bar5(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz5(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_union_of_list(use_async: bool) -> None: + assert await transform({"foo": {"foo_bar": "bar"}}, Foo5, use_async) == {"FOO": {"fooBar": "bar"}} + assert await transform( + { + "foo": [ + {"foo_baz": "baz"}, + {"foo_baz": "baz"}, + ] + }, + Foo5, + use_async, + ) == {"FOO": [{"fooBaz": "baz"}, {"fooBaz": "baz"}]} + + +class Foo6(TypedDict): + bar: Annotated[str, PropertyInfo(alias="Bar")] + + +@parametrize +@pytest.mark.asyncio +async def test_includes_unknown_keys(use_async: bool) -> None: + assert await transform({"bar": "bar", "baz_": {"FOO": 1}}, Foo6, use_async) == { + "Bar": "bar", + "baz_": {"FOO": 1}, + } + + +class Foo7(TypedDict): + bar: Annotated[List[Bar7], PropertyInfo(alias="bAr")] + foo: Bar7 + + +class Bar7(TypedDict): + foo: str + + +@parametrize +@pytest.mark.asyncio +async def test_ignores_invalid_input(use_async: bool) -> None: + assert await transform({"bar": ""}, Foo7, use_async) == {"bAr": ""} + assert await transform({"foo": ""}, Foo7, use_async) == {"foo": ""} + + +class DatetimeDict(TypedDict, total=False): + foo: Annotated[datetime, PropertyInfo(format="iso8601")] + + bar: Annotated[Optional[datetime], PropertyInfo(format="iso8601")] + + required: Required[Annotated[Optional[datetime], PropertyInfo(format="iso8601")]] + + list_: Required[Annotated[Optional[List[datetime]], PropertyInfo(format="iso8601")]] + + union: Annotated[Union[int, datetime], PropertyInfo(format="iso8601")] + + +class DateDict(TypedDict, total=False): + foo: Annotated[date, PropertyInfo(format="iso8601")] + + +class DatetimeModel(BaseModel): + foo: datetime + + +class DateModel(BaseModel): + foo: Optional[date] + + +@parametrize +@pytest.mark.asyncio +async def test_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + tz = "Z" if PYDANTIC_V2 else "+00:00" + assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] + + dt = dt.replace(tzinfo=None) + assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] + + assert await transform({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=None), Any, use_async) == {"foo": None} # type: ignore + assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=date.fromisoformat("2023-02-23")), DateDict, use_async) == { + "foo": "2023-02-23" + } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_optional_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"bar": dt}, DatetimeDict, use_async) == {"bar": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + + assert await transform({"bar": None}, DatetimeDict, use_async) == {"bar": None} + + +@parametrize +@pytest.mark.asyncio +async def test_required_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"required": dt}, DatetimeDict, use_async) == { + "required": "2023-02-23T14:16:36.337692+00:00" + } # type: ignore[comparison-overlap] + + assert await transform({"required": None}, DatetimeDict, use_async) == {"required": None} + + +@parametrize +@pytest.mark.asyncio +async def test_union_datetime(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"union": dt}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] + "union": "2023-02-23T14:16:36.337692+00:00" + } + + assert await transform({"union": "foo"}, DatetimeDict, use_async) == {"union": "foo"} + + +@parametrize +@pytest.mark.asyncio +async def test_nested_list_iso6801_format(use_async: bool) -> None: + dt1 = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + dt2 = parse_datetime("2022-01-15T06:34:23Z") + assert await transform({"list_": [dt1, dt2]}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] + "list_": ["2023-02-23T14:16:36.337692+00:00", "2022-01-15T06:34:23+00:00"] + } + + +@parametrize +@pytest.mark.asyncio +async def test_datetime_custom_format(use_async: bool) -> None: + dt = parse_datetime("2022-01-15T06:34:23Z") + + result = await transform(dt, Annotated[datetime, PropertyInfo(format="custom", format_template="%H")], use_async) + assert result == "06" # type: ignore[comparison-overlap] + + +class DateDictWithRequiredAlias(TypedDict, total=False): + required_prop: Required[Annotated[date, PropertyInfo(format="iso8601", alias="prop")]] + + +@parametrize +@pytest.mark.asyncio +async def test_datetime_with_alias(use_async: bool) -> None: + assert await transform({"required_prop": None}, DateDictWithRequiredAlias, use_async) == {"prop": None} # type: ignore[comparison-overlap] + assert await transform( + {"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias, use_async + ) == {"prop": "2023-02-23"} # type: ignore[comparison-overlap] + + +class MyModel(BaseModel): + foo: str + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_model_to_dictionary(use_async: bool) -> None: + assert cast(Any, await transform(MyModel(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + assert cast(Any, await transform(MyModel.construct(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_empty_model(use_async: bool) -> None: + assert cast(Any, await transform(MyModel.construct(), Any, use_async)) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_unknown_field(use_async: bool) -> None: + assert cast(Any, await transform(MyModel.construct(my_untyped_field=True), Any, use_async)) == { + "my_untyped_field": True + } + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_types(use_async: bool) -> None: + model = MyModel.construct(foo=True) + if PYDANTIC_V2: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + else: + params = await transform(model, Any, use_async) + assert cast(Any, params) == {"foo": True} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_object_type(use_async: bool) -> None: + model = MyModel.construct(foo=MyModel.construct(hello="world")) + if PYDANTIC_V2: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + else: + params = await transform(model, Any, use_async) + assert cast(Any, params) == {"foo": {"hello": "world"}} + + +class ModelNestedObjects(BaseModel): + nested: MyModel + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_nested_objects(use_async: bool) -> None: + model = ModelNestedObjects.construct(nested={"foo": "stainless"}) + assert isinstance(model.nested, MyModel) + assert cast(Any, await transform(model, Any, use_async)) == {"nested": {"foo": "stainless"}} + + +class ModelWithDefaultField(BaseModel): + foo: str + with_none_default: Union[str, None] = None + with_str_default: str = "foo" + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_default_field(use_async: bool) -> None: + # should be excluded when defaults are used + model = ModelWithDefaultField.construct() + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert cast(Any, await transform(model, Any, use_async)) == {} + + # should be included when the default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo") + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": None, "with_str_default": "foo"} + + # should be included when a non-default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz") + assert model.with_none_default == "bar" + assert model.with_str_default == "baz" + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": "bar", "with_str_default": "baz"} + + +class TypedDictIterableUnion(TypedDict): + foo: Annotated[Union[Bar8, Iterable[Baz8]], PropertyInfo(alias="FOO")] + + +class Bar8(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz8(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_iterable_of_dictionaries(use_async: bool) -> None: + assert await transform({"foo": [{"foo_baz": "bar"}]}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "bar"}] + } + assert cast(Any, await transform({"foo": ({"foo_baz": "bar"},)}, TypedDictIterableUnion, use_async)) == { + "FOO": [{"fooBaz": "bar"}] + } + + def my_iter() -> Iterable[Baz8]: + yield {"foo_baz": "hello"} + yield {"foo_baz": "world"} + + assert await transform({"foo": my_iter()}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "hello"}, {"fooBaz": "world"}] + } + + +@parametrize +@pytest.mark.asyncio +async def test_dictionary_items(use_async: bool) -> None: + class DictItems(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} + + +class TypedDictIterableUnionStr(TypedDict): + foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")] + + +@parametrize +@pytest.mark.asyncio +async def test_iterable_union_str(use_async: bool) -> None: + assert await transform({"foo": "bar"}, TypedDictIterableUnionStr, use_async) == {"FOO": "bar"} + assert cast(Any, await transform(iter([{"foo_baz": "bar"}]), Union[str, Iterable[Baz8]], use_async)) == [ + {"fooBaz": "bar"} + ] + + +class TypedDictBase64Input(TypedDict): + foo: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")] + + +@parametrize +@pytest.mark.asyncio +async def test_base64_file_input(use_async: bool) -> None: + # strings are left as-is + assert await transform({"foo": "bar"}, TypedDictBase64Input, use_async) == {"foo": "bar"} + + # pathlib.Path is automatically converted to base64 + assert await transform({"foo": SAMPLE_FILE_PATH}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQo=" + } # type: ignore[comparison-overlap] + + # io instances are automatically converted to base64 + assert await transform({"foo": io.StringIO("Hello, world!")}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQ==" + } # type: ignore[comparison-overlap] + assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQ==" + } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_transform_skipping(use_async: bool) -> None: + # lists of ints are left as-is + data = [1, 2, 3] + assert await transform(data, List[int], use_async) is data + + # iterables of ints are converted to a list + data = iter([1, 2, 3]) + assert await transform(data, Iterable[int], use_async) == [1, 2, 3] + + +@parametrize +@pytest.mark.asyncio +async def test_strips_notgiven(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {} diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py new file mode 100644 index 0000000..aaac504 --- /dev/null +++ b/tests/test_utils/test_proxy.py @@ -0,0 +1,34 @@ +import operator +from typing import Any +from typing_extensions import override + +from cas_parser._utils import LazyProxy + + +class RecursiveLazyProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + return self + + def __call__(self, *_args: Any, **_kwds: Any) -> Any: + raise RuntimeError("This should never be called!") + + +def test_recursive_proxy() -> None: + proxy = RecursiveLazyProxy() + assert repr(proxy) == "RecursiveLazyProxy" + assert str(proxy) == "RecursiveLazyProxy" + assert dir(proxy) == [] + assert type(proxy).__name__ == "RecursiveLazyProxy" + assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" + + +def test_isinstance_does_not_error() -> None: + class AlwaysErrorProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + raise RuntimeError("Mocking missing dependency") + + proxy = AlwaysErrorProxy() + assert not isinstance(proxy, dict) + assert isinstance(proxy, LazyProxy) diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py new file mode 100644 index 0000000..cc81657 --- /dev/null +++ b/tests/test_utils/test_typing.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from typing import Generic, TypeVar, cast + +from cas_parser._utils import extract_type_var_from_base + +_T = TypeVar("_T") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") + + +class BaseGeneric(Generic[_T]): ... + + +class SubclassGeneric(BaseGeneric[_T]): ... + + +class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ... + + +class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ... + + +class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ... + + +def test_extract_type_var() -> None: + assert ( + extract_type_var_from_base( + BaseGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_generic_subclass() -> None: + assert ( + extract_type_var_from_base( + SubclassGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_multiple() -> None: + typ = BaseGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_multiple() -> None: + typ = SubclassGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_different_ordering_multiple() -> None: + typ = SubclassDifferentOrderGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..20c2774 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import os +import inspect +import traceback +import contextlib +from typing import Any, TypeVar, Iterator, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, get_origin, assert_type + +from cas_parser._types import Omit, NoneType +from cas_parser._utils import ( + is_dict, + is_list, + is_list_type, + is_union_type, + extract_type_arg, + is_annotated_type, + is_type_alias_type, +) +from cas_parser._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from cas_parser._models import BaseModel + +BaseModelT = TypeVar("BaseModelT", bound=BaseModel) + + +def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: + for name, field in get_model_fields(model).items(): + field_value = getattr(value, name) + if PYDANTIC_V2: + allow_none = False + else: + # in v1 nullability was structured differently + # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields + allow_none = getattr(field, "allow_none", False) + + assert_matches_type( + field_outer_type(field), + field_value, + path=[*path, name], + allow_none=allow_none, + ) + + return True + + +# Note: the `path` argument is only used to improve error messages when `--showlocals` is used +def assert_matches_type( + type_: Any, + value: object, + *, + path: list[str], + allow_none: bool = False, +) -> None: + if is_type_alias_type(type_): + type_ = type_.__value__ + + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(type_): + type_ = extract_type_arg(type_, 0) + + if allow_none and value is None: + return + + if type_ is None or type_ is NoneType: + assert value is None + return + + origin = get_origin(type_) or type_ + + if is_list_type(type_): + return _assert_list_type(type_, value) + + if origin == str: + assert isinstance(value, str) + elif origin == int: + assert isinstance(value, int) + elif origin == bool: + assert isinstance(value, bool) + elif origin == float: + assert isinstance(value, float) + elif origin == bytes: + assert isinstance(value, bytes) + elif origin == datetime: + assert isinstance(value, datetime) + elif origin == date: + assert isinstance(value, date) + elif origin == object: + # nothing to do here, the expected type is unknown + pass + elif origin == Literal: + assert value in get_args(type_) + elif origin == dict: + assert is_dict(value) + + args = get_args(type_) + key_type = args[0] + items_type = args[1] + + for key, item in value.items(): + assert_matches_type(key_type, key, path=[*path, ""]) + assert_matches_type(items_type, item, path=[*path, ""]) + elif is_union_type(type_): + variants = get_args(type_) + + try: + none_index = variants.index(type(None)) + except ValueError: + pass + else: + # special case Optional[T] for better error messages + if len(variants) == 2: + if value is None: + # valid + return + + return assert_matches_type(type_=variants[not none_index], value=value, path=path) + + for i, variant in enumerate(variants): + try: + assert_matches_type(variant, value, path=[*path, f"variant {i}"]) + return + except AssertionError: + traceback.print_exc() + continue + + raise AssertionError("Did not match any variants") + elif issubclass(origin, BaseModel): + assert isinstance(value, type_) + assert assert_matches_model(type_, cast(Any, value), path=path) + elif inspect.isclass(origin) and origin.__name__ == "HttpxBinaryResponseContent": + assert value.__class__.__name__ == "HttpxBinaryResponseContent" + else: + assert None, f"Unhandled field type: {type_}" + + +def _assert_list_type(type_: type[object], value: object) -> None: + assert is_list(value) + + inner_type = get_args(type_)[0] + for entry in value: + assert_type(inner_type, entry) # type: ignore + + +@contextlib.contextmanager +def update_env(**new_env: str | Omit) -> Iterator[None]: + old = os.environ.copy() + + try: + for name, value in new_env.items(): + if isinstance(value, Omit): + os.environ.pop(name, None) + else: + os.environ[name] = value + + yield None + finally: + os.environ.clear() + os.environ.update(old) From a32c49283386087da0d17b73fba047d26ac3da55 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 06:33:25 +0000 Subject: [PATCH 002/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 36105c2..b0bed10 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: e5b8a95b93a04cfe1a8b6546333954ac +config_hash: 2632d49bed76591e5ca0ddf94f518f17 From e18404e1b65fdaf2a922a3e1153cac95c2d9f851 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 06:34:37 +0000 Subject: [PATCH 003/116] chore: update SDK settings --- .github/workflows/publish-pypi.yml | 31 +++++++++++ .github/workflows/release-doctor.yml | 21 ++++++++ .release-please-manifest.json | 3 ++ .stats.yml | 2 +- CONTRIBUTING.md | 4 +- README.md | 14 ++--- bin/check-release-environment | 21 ++++++++ pyproject.toml | 6 +-- release-please-config.json | 66 +++++++++++++++++++++++ src/cas_parser/_version.py | 2 +- src/cas_parser/resources/cas_generator.py | 8 +-- src/cas_parser/resources/cas_parser.py | 8 +-- 12 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/publish-pypi.yml create mode 100644 .github/workflows/release-doctor.yml create mode 100644 .release-please-manifest.json create mode 100644 bin/check-release-environment create mode 100644 release-please-config.json diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 0000000..f0a5b3c --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,31 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to PyPI in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/CASParser/cas-parser-python/actions/workflows/publish-pypi.yml +name: Publish PyPI +on: + workflow_dispatch: + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Publish to PyPI + run: | + bash ./bin/publish-pypi + env: + PYPI_TOKEN: ${{ secrets.CAS_PARSER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..ea04f96 --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,21 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'CASParser/cas-parser-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v4 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + PYPI_TOKEN: ${{ secrets.CAS_PARSER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..1332969 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index b0bed10..4a4c195 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: 2632d49bed76591e5ca0ddf94f518f17 +config_hash: 1de8a243a3962065e289ca915dfc6127 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c565215..9de374b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```sh -$ pip install git+ssh://git@github.com/stainless-sdks/cas-parser-python.git +$ pip install git+ssh://git@github.com/CASParser/cas-parser-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -120,7 +120,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/cas-parser-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/CASParser/cas-parser-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index 0843fd6..c139318 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ The REST API documentation can be found on [docs.casparser.in](https://docs.casp ## Installation ```sh -# install from this staging repo -pip install git+ssh://git@github.com/stainless-sdks/cas-parser-python.git +# install from the production repo +pip install git+ssh://git@github.com/CASParser/cas-parser-python.git ``` > [!NOTE] @@ -79,8 +79,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from this staging repo -pip install 'cas_parser[aiohttp] @ git+ssh://git@github.com/stainless-sdks/cas-parser-python.git' +# install from the production repo +pip install 'cas_parser[aiohttp] @ git+ssh://git@github.com/CASParser/cas-parser-python.git' ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: @@ -242,9 +242,9 @@ cas_parser = response.parse() # get the object that `cas_parser.cams_kfintech() print(cas_parser.demat_accounts) ``` -These methods return an [`APIResponse`](https://github.com/stainless-sdks/cas-parser-python/tree/main/src/cas_parser/_response.py) object. +These methods return an [`APIResponse`](https://github.com/CASParser/cas-parser-python/tree/main/src/cas_parser/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/cas-parser-python/tree/main/src/cas_parser/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/CASParser/cas-parser-python/tree/main/src/cas_parser/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -348,7 +348,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/cas-parser-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/CASParser/cas-parser-python/issues) with questions, bugs, or suggestions. ### Determining the installed version diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..b845b0f --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${PYPI_TOKEN}" ]; then + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/pyproject.toml b/pyproject.toml index 2c2af13..5faf1eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,8 +35,8 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/stainless-sdks/cas-parser-python" -Repository = "https://github.com/stainless-sdks/cas-parser-python" +Homepage = "https://github.com/CASParser/cas-parser-python" +Repository = "https://github.com/CASParser/cas-parser-python" [project.optional-dependencies] aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] @@ -125,7 +125,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/stainless-sdks/cas-parser-python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/CASParser/cas-parser-python/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..316db21 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,66 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "python", + "extra-files": [ + "src/cas_parser/_version.py" + ] +} \ No newline at end of file diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 96a609e..f0dea59 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "0.0.1" +__version__ = "0.0.1" # x-release-please-version diff --git a/src/cas_parser/resources/cas_generator.py b/src/cas_parser/resources/cas_generator.py index 77bc291..511b893 100644 --- a/src/cas_parser/resources/cas_generator.py +++ b/src/cas_parser/resources/cas_generator.py @@ -30,7 +30,7 @@ def with_raw_response(self) -> CasGeneratorResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return CasGeneratorResourceWithRawResponse(self) @@ -39,7 +39,7 @@ def with_streaming_response(self) -> CasGeneratorResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return CasGeneratorResourceWithStreamingResponse(self) @@ -113,7 +113,7 @@ def with_raw_response(self) -> AsyncCasGeneratorResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncCasGeneratorResourceWithRawResponse(self) @@ -122,7 +122,7 @@ def with_streaming_response(self) -> AsyncCasGeneratorResourceWithStreamingRespo """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncCasGeneratorResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/cas_parser.py b/src/cas_parser/resources/cas_parser.py index 45d205a..a64b7dd 100644 --- a/src/cas_parser/resources/cas_parser.py +++ b/src/cas_parser/resources/cas_parser.py @@ -35,7 +35,7 @@ def with_raw_response(self) -> CasParserResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return CasParserResourceWithRawResponse(self) @@ -44,7 +44,7 @@ def with_streaming_response(self) -> CasParserResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return CasParserResourceWithStreamingResponse(self) @@ -281,7 +281,7 @@ def with_raw_response(self) -> AsyncCasParserResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncCasParserResourceWithRawResponse(self) @@ -290,7 +290,7 @@ def with_streaming_response(self) -> AsyncCasParserResourceWithStreamingResponse """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncCasParserResourceWithStreamingResponse(self) From 30c5069b46879bf955e6f5fb097ce7a72cf7c3aa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:01:49 +0000 Subject: [PATCH 004/116] feat(api): manual updates --- .stats.yml | 2 +- README.md | 46 +++++++++++++++------ src/cas_parser/__init__.py | 2 - src/cas_parser/_client.py | 82 ++++++-------------------------------- tests/test_client.py | 60 ++++++++++------------------ 5 files changed, 66 insertions(+), 126 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4a4c195..58a5c1f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: 1de8a243a3962065e289ca915dfc6127 +config_hash: d9c1f7b95d5659724df3e026c4fab291 diff --git a/README.md b/README.md index c139318..1b3cffb 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,12 @@ from cas_parser import CasParser client = CasParser( api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted - # defaults to "production". - environment="local", ) -unified_response = client.cas_parser.cams_kfintech() +unified_response = client.cas_parser.smart_parse( + password="ABCDF", + pdf_url="https://your-cas-pdf-url-here.com", +) print(unified_response.demat_accounts) ``` @@ -57,13 +58,14 @@ from cas_parser import AsyncCasParser client = AsyncCasParser( api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted - # defaults to "production". - environment="local", ) async def main() -> None: - unified_response = await client.cas_parser.cams_kfintech() + unified_response = await client.cas_parser.smart_parse( + password="ABCDF", + pdf_url="https://your-cas-pdf-url-here.com", + ) print(unified_response.demat_accounts) @@ -96,7 +98,10 @@ async def main() -> None: api_key="My API Key", http_client=DefaultAioHttpClient(), ) as client: - unified_response = await client.cas_parser.cams_kfintech() + unified_response = await client.cas_parser.smart_parse( + password="ABCDF", + pdf_url="https://your-cas-pdf-url-here.com", + ) print(unified_response.demat_accounts) @@ -128,7 +133,10 @@ from cas_parser import CasParser client = CasParser() try: - client.cas_parser.cams_kfintech() + client.cas_parser.smart_parse( + password="ABCDF", + pdf_url="https://you-cas-pdf-url-here.com", + ) except cas_parser.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. @@ -171,7 +179,10 @@ client = CasParser( ) # Or, configure per-request: -client.with_options(max_retries=5).cas_parser.cams_kfintech() +client.with_options(max_retries=5).cas_parser.smart_parse( + password="ABCDF", + pdf_url="https://you-cas-pdf-url-here.com", +) ``` ### Timeouts @@ -194,7 +205,10 @@ client = CasParser( ) # Override per-request: -client.with_options(timeout=5.0).cas_parser.cams_kfintech() +client.with_options(timeout=5.0).cas_parser.smart_parse( + password="ABCDF", + pdf_url="https://you-cas-pdf-url-here.com", +) ``` On timeout, an `APITimeoutError` is thrown. @@ -235,10 +249,13 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from cas_parser import CasParser client = CasParser() -response = client.cas_parser.with_raw_response.cams_kfintech() +response = client.cas_parser.with_raw_response.smart_parse( + password="ABCDF", + pdf_url="https://you-cas-pdf-url-here.com", +) print(response.headers.get('X-My-Header')) -cas_parser = response.parse() # get the object that `cas_parser.cams_kfintech()` would have returned +cas_parser = response.parse() # get the object that `cas_parser.smart_parse()` would have returned print(cas_parser.demat_accounts) ``` @@ -253,7 +270,10 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.cas_parser.with_streaming_response.cams_kfintech() as response: +with client.cas_parser.with_streaming_response.smart_parse( + password="ABCDF", + pdf_url="https://you-cas-pdf-url-here.com", +) as response: print(response.headers.get("X-My-Header")) for line in response.iter_lines(): diff --git a/src/cas_parser/__init__.py b/src/cas_parser/__init__.py index 25d271f..a6c342f 100644 --- a/src/cas_parser/__init__.py +++ b/src/cas_parser/__init__.py @@ -6,7 +6,6 @@ from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path from ._client import ( - ENVIRONMENTS, Client, Stream, Timeout, @@ -72,7 +71,6 @@ "AsyncStream", "CasParser", "AsyncCasParser", - "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py index 2bd3fc9..27572c6 100644 --- a/src/cas_parser/_client.py +++ b/src/cas_parser/_client.py @@ -3,8 +3,8 @@ from __future__ import annotations import os -from typing import Any, Dict, Union, Mapping, cast -from typing_extensions import Self, Literal, override +from typing import Any, Union, Mapping +from typing_extensions import Self, override import httpx @@ -31,7 +31,6 @@ ) __all__ = [ - "ENVIRONMENTS", "Timeout", "Transport", "ProxiesTypes", @@ -42,11 +41,6 @@ "AsyncClient", ] -ENVIRONMENTS: Dict[str, str] = { - "production": "https://portfolio-parser.api.casparser.in", - "local": "http://localhost:5000", -} - class CasParser(SyncAPIClient): cas_parser: cas_parser.CasParserResource @@ -57,14 +51,11 @@ class CasParser(SyncAPIClient): # client options api_key: str - _environment: Literal["production", "local"] | NotGiven - def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "local"] | NotGiven = NOT_GIVEN, - base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None = None, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -95,31 +86,10 @@ def __init__( ) self.api_key = api_key - self._environment = environment - - base_url_env = os.environ.get("CAS_PARSER_BASE_URL") - if is_given(base_url) and base_url is not None: - # cast required because mypy doesn't understand the type narrowing - base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] - elif is_given(environment): - if base_url_env and base_url is not None: - raise ValueError( - "Ambiguous URL; The `CAS_PARSER_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", - ) - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc - elif base_url_env is not None: - base_url = base_url_env - else: - self._environment = environment = "production" - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc + if base_url is None: + base_url = os.environ.get("CAS_PARSER_BASE_URL") + if base_url is None: + base_url = f"https://portfolio-parser.api.casparser.in" super().__init__( version=__version__, @@ -161,7 +131,6 @@ def copy( self, *, api_key: str | None = None, - environment: Literal["production", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.Client | None = None, @@ -197,7 +166,6 @@ def copy( return self.__class__( api_key=api_key or self.api_key, base_url=base_url or self.base_url, - environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, @@ -253,14 +221,11 @@ class AsyncCasParser(AsyncAPIClient): # client options api_key: str - _environment: Literal["production", "local"] | NotGiven - def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "local"] | NotGiven = NOT_GIVEN, - base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None = None, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -291,31 +256,10 @@ def __init__( ) self.api_key = api_key - self._environment = environment - - base_url_env = os.environ.get("CAS_PARSER_BASE_URL") - if is_given(base_url) and base_url is not None: - # cast required because mypy doesn't understand the type narrowing - base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] - elif is_given(environment): - if base_url_env and base_url is not None: - raise ValueError( - "Ambiguous URL; The `CAS_PARSER_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", - ) - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc - elif base_url_env is not None: - base_url = base_url_env - else: - self._environment = environment = "production" - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc + if base_url is None: + base_url = os.environ.get("CAS_PARSER_BASE_URL") + if base_url is None: + base_url = f"https://portfolio-parser.api.casparser.in" super().__init__( version=__version__, @@ -357,7 +301,6 @@ def copy( self, *, api_key: str | None = None, - environment: Literal["production", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.AsyncClient | None = None, @@ -393,7 +336,6 @@ def copy( return self.__class__( api_key=api_key or self.api_key, base_url=base_url or self.base_url, - environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, diff --git a/tests/test_client.py b/tests/test_client.py index 5b83c72..201c609 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -560,16 +560,6 @@ def test_base_url_env(self) -> None: client = CasParser(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" - # explicit environment arg requires explicitness - with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): - with pytest.raises(ValueError, match=r"you must pass base_url=None"): - CasParser(api_key=api_key, _strict_response_validation=True, environment="production") - - client = CasParser( - base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" - ) - assert str(client.base_url).startswith("https://portfolio-parser.api.casparser.in") - @pytest.mark.parametrize( "client", [ @@ -724,20 +714,20 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: CasParser) -> None: - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v4/smart/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.cas_parser.with_streaming_response.cams_kfintech().__enter__() + client.cas_parser.with_streaming_response.smart_parse().__enter__() assert _get_open_connections(self.client) == 0 @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: CasParser) -> None: - respx_mock.post("/v4/cams_kfintech/parse").mock(return_value=httpx.Response(500)) + respx_mock.post("/v4/smart/parse").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.cas_parser.with_streaming_response.cams_kfintech().__enter__() + client.cas_parser.with_streaming_response.smart_parse().__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -764,9 +754,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) - response = client.cas_parser.with_raw_response.cams_kfintech() + response = client.cas_parser.with_raw_response.smart_parse() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -788,9 +778,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) - response = client.cas_parser.with_raw_response.cams_kfintech(extra_headers={"x-stainless-retry-count": Omit()}) + response = client.cas_parser.with_raw_response.smart_parse(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -811,9 +801,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) - response = client.cas_parser.with_raw_response.cams_kfintech(extra_headers={"x-stainless-retry-count": "42"}) + response = client.cas_parser.with_raw_response.smart_parse(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1373,16 +1363,6 @@ def test_base_url_env(self) -> None: client = AsyncCasParser(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" - # explicit environment arg requires explicitness - with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): - with pytest.raises(ValueError, match=r"you must pass base_url=None"): - AsyncCasParser(api_key=api_key, _strict_response_validation=True, environment="production") - - client = AsyncCasParser( - base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" - ) - assert str(client.base_url).startswith("https://portfolio-parser.api.casparser.in") - @pytest.mark.parametrize( "client", [ @@ -1551,10 +1531,10 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte async def test_retrying_timeout_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncCasParser ) -> None: - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v4/smart/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.cas_parser.with_streaming_response.cams_kfintech().__aenter__() + await async_client.cas_parser.with_streaming_response.smart_parse().__aenter__() assert _get_open_connections(self.client) == 0 @@ -1563,10 +1543,10 @@ async def test_retrying_timeout_errors_doesnt_leak( async def test_retrying_status_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncCasParser ) -> None: - respx_mock.post("/v4/cams_kfintech/parse").mock(return_value=httpx.Response(500)) + respx_mock.post("/v4/smart/parse").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.cas_parser.with_streaming_response.cams_kfintech().__aenter__() + await async_client.cas_parser.with_streaming_response.smart_parse().__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1594,9 +1574,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) - response = await client.cas_parser.with_raw_response.cams_kfintech() + response = await client.cas_parser.with_raw_response.smart_parse() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1619,9 +1599,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) - response = await client.cas_parser.with_raw_response.cams_kfintech( + response = await client.cas_parser.with_raw_response.smart_parse( extra_headers={"x-stainless-retry-count": Omit()} ) @@ -1645,9 +1625,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) - response = await client.cas_parser.with_raw_response.cams_kfintech( + response = await client.cas_parser.with_raw_response.smart_parse( extra_headers={"x-stainless-retry-count": "42"} ) From e4b37c35d25055d53c72f465385c9c03ae2bd3e8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:14:19 +0000 Subject: [PATCH 005/116] chore: update SDK settings --- .stats.yml | 2 +- README.md | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index 58a5c1f..ff342c7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: d9c1f7b95d5659724df3e026c4fab291 +config_hash: a09a453fc58f48d89b5b8fce49bfb354 diff --git a/README.md b/README.md index 1b3cffb..933b01b 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,10 @@ The REST API documentation can be found on [docs.casparser.in](https://docs.casp ## Installation ```sh -# install from the production repo -pip install git+ssh://git@github.com/CASParser/cas-parser-python.git +# install from PyPI +pip install cas_parser ``` -> [!NOTE] -> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install cas_parser` - ## Usage The full API of this library can be found in [api.md](api.md). @@ -81,8 +78,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from the production repo -pip install 'cas_parser[aiohttp] @ git+ssh://git@github.com/CASParser/cas-parser-python.git' +# install from PyPI +pip install cas_parser[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: From 0d830c3074073a4ab8afb4e573bf340c622e76fd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:27:12 +0000 Subject: [PATCH 006/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1332969..fea3454 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1" + ".": "1.0.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5faf1eb..df5dc8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas_parser" -version = "0.0.1" +version = "1.0.0" description = "The official Python library for the CAS Parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index f0dea59..6e734e4 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "0.0.1" # x-release-please-version +__version__ = "1.0.0" # x-release-please-version From 2c1c7e9c08e638447c1541b7f6957496cbd5d4df Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:32:26 +0000 Subject: [PATCH 007/116] chore: update SDK settings --- .stats.yml | 2 +- README.md | 6 +++--- pyproject.toml | 2 +- requirements-dev.lock | 16 ++++++++-------- requirements.lock | 16 ++++++++-------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.stats.yml b/.stats.yml index ff342c7..a8acd5d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: a09a453fc58f48d89b5b8fce49bfb354 +config_hash: 3b87fe01dc5604f4c7ae72e273509802 diff --git a/README.md b/README.md index 933b01b..c13a2c4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Cas Parser Python API library -[![PyPI version](https://img.shields.io/pypi/v/cas_parser.svg?label=pypi%20(stable))](https://pypi.org/project/cas_parser/) +[![PyPI version](https://img.shields.io/pypi/v/cas-parser-python.svg?label=pypi%20(stable))](https://pypi.org/project/cas-parser-python/) The Cas Parser Python library provides convenient access to the Cas Parser REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, @@ -17,7 +17,7 @@ The REST API documentation can be found on [docs.casparser.in](https://docs.casp ```sh # install from PyPI -pip install cas_parser +pip install cas-parser-python ``` ## Usage @@ -79,7 +79,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install cas_parser[aiohttp] +pip install cas-parser-python[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index df5dc8c..fafcb33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "cas_parser" +name = "cas-parser-python" version = "1.0.0" description = "The official Python library for the CAS Parser API" dynamic = ["readme"] diff --git a/requirements-dev.lock b/requirements-dev.lock index ea03aeb..2625645 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -13,14 +13,14 @@ aiohappyeyeballs==2.6.1 # via aiohttp aiohttp==3.12.8 - # via cas-parser + # via cas-parser-python # via httpx-aiohttp aiosignal==1.3.2 # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 - # via cas-parser + # via cas-parser-python # via httpx argcomplete==3.1.2 # via nox @@ -37,7 +37,7 @@ dirty-equals==0.6.0 distlib==0.3.7 # via virtualenv distro==1.8.0 - # via cas-parser + # via cas-parser-python exceptiongroup==1.2.2 # via anyio # via pytest @@ -53,11 +53,11 @@ h11==0.16.0 httpcore==1.0.9 # via httpx httpx==0.28.1 - # via cas-parser + # via cas-parser-python # via httpx-aiohttp # via respx httpx-aiohttp==0.1.8 - # via cas-parser + # via cas-parser-python idna==3.4 # via anyio # via httpx @@ -90,7 +90,7 @@ propcache==0.3.1 # via aiohttp # via yarl pydantic==2.10.3 - # via cas-parser + # via cas-parser-python pydantic-core==2.27.1 # via pydantic pygments==2.18.0 @@ -114,14 +114,14 @@ six==1.16.0 # via python-dateutil sniffio==1.3.0 # via anyio - # via cas-parser + # via cas-parser-python time-machine==2.9.0 tomli==2.0.2 # via mypy # via pytest typing-extensions==4.12.2 # via anyio - # via cas-parser + # via cas-parser-python # via multidict # via mypy # via pydantic diff --git a/requirements.lock b/requirements.lock index 3de2b6b..46d36df 100644 --- a/requirements.lock +++ b/requirements.lock @@ -13,14 +13,14 @@ aiohappyeyeballs==2.6.1 # via aiohttp aiohttp==3.12.8 - # via cas-parser + # via cas-parser-python # via httpx-aiohttp aiosignal==1.3.2 # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 - # via cas-parser + # via cas-parser-python # via httpx async-timeout==5.0.1 # via aiohttp @@ -30,7 +30,7 @@ certifi==2023.7.22 # via httpcore # via httpx distro==1.8.0 - # via cas-parser + # via cas-parser-python exceptiongroup==1.2.2 # via anyio frozenlist==1.6.2 @@ -41,10 +41,10 @@ h11==0.16.0 httpcore==1.0.9 # via httpx httpx==0.28.1 - # via cas-parser + # via cas-parser-python # via httpx-aiohttp httpx-aiohttp==0.1.8 - # via cas-parser + # via cas-parser-python idna==3.4 # via anyio # via httpx @@ -56,15 +56,15 @@ propcache==0.3.1 # via aiohttp # via yarl pydantic==2.10.3 - # via cas-parser + # via cas-parser-python pydantic-core==2.27.1 # via pydantic sniffio==1.3.0 # via anyio - # via cas-parser + # via cas-parser-python typing-extensions==4.12.2 # via anyio - # via cas-parser + # via cas-parser-python # via multidict # via pydantic # via pydantic-core From 01a8ce888cecd749e1c8cf32fde406c1ba824604 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:34:01 +0000 Subject: [PATCH 008/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fea3454..a8f7122 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.0" + ".": "1.0.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index fafcb33..a345145 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.0.0" +version = "1.0.1" description = "The official Python library for the CAS Parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 6e734e4..c9e9fb0 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.0.0" # x-release-please-version +__version__ = "1.0.1" # x-release-please-version From a2edd3cbcb57d59b14a513030dc1a253b48edd34 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:07:16 +0000 Subject: [PATCH 009/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index a8acd5d..aa8116c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: 3b87fe01dc5604f4c7ae72e273509802 +config_hash: 05034d55bf9f8573b1160f7825bcd9b9 From 22b6b68e8fc34bc97f42a81d1ea8c0ba000a2d69 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:07:38 +0000 Subject: [PATCH 010/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index aa8116c..af747c5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: 05034d55bf9f8573b1160f7825bcd9b9 +config_hash: d34508a94d94e1155705c01231bd6f17 From 16020d8a61ee0d31c0de1167cfb26201e723f4d4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:10:43 +0000 Subject: [PATCH 011/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index af747c5..0caf7fc 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: d34508a94d94e1155705c01231bd6f17 +config_hash: 0e1291f316b20497ad29b59a231a8680 From a7714daa68358ed193a0731f75aae65bd6779a00 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:01:10 +0000 Subject: [PATCH 012/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 0caf7fc..6566ecd 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: 0e1291f316b20497ad29b59a231a8680 +config_hash: 7e37f38c64335d64bc0f2bdd0a655a97 From d2d6536968d64b162aab7111fd4385a288e4b9d2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:02:51 +0000 Subject: [PATCH 013/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 6566ecd..92721c7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff -config_hash: 7e37f38c64335d64bc0f2bdd0a655a97 +config_hash: cb5d75abef6264b5d86448caf7295afa From 135dfb87f45027288ca4e808b27e4ef559244ebf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 04:48:25 +0000 Subject: [PATCH 014/116] chore: update github action --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d5023d..2832d52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: run: ./scripts/lint build: - if: github.repository == 'stainless-sdks/cas-parser-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork timeout-minutes: 10 name: build permissions: @@ -61,12 +61,14 @@ jobs: run: rye build - name: Get GitHub OIDC Token + if: github.repository == 'stainless-sdks/cas-parser-python' id: github-oidc uses: actions/github-script@v6 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball + if: github.repository == 'stainless-sdks/cas-parser-python' env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From 597778467b8dd0520406ad6f87a56d590a88bc56 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 03:55:39 +0000 Subject: [PATCH 015/116] chore(internal): change ci workflow machines --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2832d52..3a9eb32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: permissions: contents: read id-token: write - runs-on: depot-ubuntu-24.04 + runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 From b1dc2380dafab48d4c80c87d47d33f7bbfdd6d32 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 05:11:42 +0000 Subject: [PATCH 016/116] fix: avoid newer type syntax --- src/cas_parser/_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cas_parser/_models.py b/src/cas_parser/_models.py index b8387ce..92f7c10 100644 --- a/src/cas_parser/_models.py +++ b/src/cas_parser/_models.py @@ -304,7 +304,7 @@ def model_dump( exclude_none=exclude_none, ) - return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped + return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped @override def model_dump_json( From aac86e9ba24feff0a1f8bbd714445f46672c81f6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 05:31:14 +0000 Subject: [PATCH 017/116] chore(internal): update pyright exclude list --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a345145..0136197 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,6 +148,7 @@ exclude = [ "_dev", ".venv", ".nox", + ".git", ] reportImplicitOverride = true From 243d23b5909f32b6c3cb2b478b0842fdd3c41d62 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:08:54 +0000 Subject: [PATCH 018/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a8f7122..06d6df2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.1" + ".": "1.0.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0136197..3ddf740 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.0.1" +version = "1.0.2" description = "The official Python library for the CAS Parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index c9e9fb0..383444a 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.0.1" # x-release-please-version +__version__ = "1.0.2" # x-release-please-version From 14d005c6d1d5dc03a73ea9a07c3620cf0f5d4978 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 30 Aug 2025 03:07:10 +0000 Subject: [PATCH 019/116] chore(internal): add Sequence related utils --- src/cas_parser/_types.py | 36 ++++++++++++++++++++++++++++++- src/cas_parser/_utils/__init__.py | 1 + src/cas_parser/_utils/_typing.py | 5 +++++ tests/utils.py | 10 ++++++++- 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/cas_parser/_types.py b/src/cas_parser/_types.py index 87f9f3d..920e967 100644 --- a/src/cas_parser/_types.py +++ b/src/cas_parser/_types.py @@ -13,10 +13,21 @@ Mapping, TypeVar, Callable, + Iterator, Optional, Sequence, ) -from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable +from typing_extensions import ( + Set, + Literal, + Protocol, + TypeAlias, + TypedDict, + SupportsIndex, + overload, + override, + runtime_checkable, +) import httpx import pydantic @@ -217,3 +228,26 @@ class _GenericAlias(Protocol): class HttpxSendArgs(TypedDict, total=False): auth: httpx.Auth follow_redirects: bool + + +_T_co = TypeVar("_T_co", covariant=True) + + +if TYPE_CHECKING: + # This works because str.__contains__ does not accept object (either in typeshed or at runtime) + # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + class SequenceNotStr(Protocol[_T_co]): + @overload + def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... + @overload + def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... + def __contains__(self, value: object, /) -> bool: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[_T_co]: ... + def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... + def count(self, value: Any, /) -> int: ... + def __reversed__(self) -> Iterator[_T_co]: ... +else: + # just point this to a normal `Sequence` at runtime to avoid having to special case + # deserializing our custom sequence type + SequenceNotStr = Sequence diff --git a/src/cas_parser/_utils/__init__.py b/src/cas_parser/_utils/__init__.py index d4fda26..ca547ce 100644 --- a/src/cas_parser/_utils/__init__.py +++ b/src/cas_parser/_utils/__init__.py @@ -38,6 +38,7 @@ extract_type_arg as extract_type_arg, is_iterable_type as is_iterable_type, is_required_type as is_required_type, + is_sequence_type as is_sequence_type, is_annotated_type as is_annotated_type, is_type_alias_type as is_type_alias_type, strip_annotated_type as strip_annotated_type, diff --git a/src/cas_parser/_utils/_typing.py b/src/cas_parser/_utils/_typing.py index 1bac954..845cd6b 100644 --- a/src/cas_parser/_utils/_typing.py +++ b/src/cas_parser/_utils/_typing.py @@ -26,6 +26,11 @@ def is_list_type(typ: type) -> bool: return (get_origin(typ) or typ) == list +def is_sequence_type(typ: type) -> bool: + origin = get_origin(typ) or typ + return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence + + def is_iterable_type(typ: type) -> bool: """If the given type is `typing.Iterable[T]`""" origin = get_origin(typ) or typ diff --git a/tests/utils.py b/tests/utils.py index 20c2774..4df38ad 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,7 +4,7 @@ import inspect import traceback import contextlib -from typing import Any, TypeVar, Iterator, cast +from typing import Any, TypeVar, Iterator, Sequence, cast from datetime import date, datetime from typing_extensions import Literal, get_args, get_origin, assert_type @@ -15,6 +15,7 @@ is_list_type, is_union_type, extract_type_arg, + is_sequence_type, is_annotated_type, is_type_alias_type, ) @@ -71,6 +72,13 @@ def assert_matches_type( if is_list_type(type_): return _assert_list_type(type_, value) + if is_sequence_type(type_): + assert isinstance(value, Sequence) + inner_type = get_args(type_)[0] + for entry in value: # type: ignore + assert_type(inner_type, entry) # type: ignore + return + if origin == str: assert isinstance(value, str) elif origin == int: From 3a6df74768b37626087528d88854535f7b39e1f4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 02:46:05 +0000 Subject: [PATCH 020/116] feat(types): replace List[str] with SequenceNotStr in params --- src/cas_parser/_utils/_transform.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cas_parser/_utils/_transform.py b/src/cas_parser/_utils/_transform.py index b0cc20a..f0bcefd 100644 --- a/src/cas_parser/_utils/_transform.py +++ b/src/cas_parser/_utils/_transform.py @@ -16,6 +16,7 @@ lru_cache, is_mapping, is_iterable, + is_sequence, ) from .._files import is_base64_file_input from ._typing import ( @@ -24,6 +25,7 @@ extract_type_arg, is_iterable_type, is_required_type, + is_sequence_type, is_annotated_type, strip_annotated_type, ) @@ -184,6 +186,8 @@ def _transform_recursive( (is_list_type(stripped_type) and is_list(data)) # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) ): # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually # intended as an iterable, so we don't transform it. @@ -346,6 +350,8 @@ async def _async_transform_recursive( (is_list_type(stripped_type) and is_list(data)) # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) ): # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually # intended as an iterable, so we don't transform it. From 4d804119e8b2cd3f73222385ce9733230742aba2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 02:50:35 +0000 Subject: [PATCH 021/116] feat: improve future compat with pydantic v3 --- src/cas_parser/_base_client.py | 6 +- src/cas_parser/_compat.py | 96 ++++++++-------- src/cas_parser/_models.py | 80 ++++++------- src/cas_parser/_utils/__init__.py | 10 +- src/cas_parser/_utils/_compat.py | 45 ++++++++ src/cas_parser/_utils/_datetime_parse.py | 136 +++++++++++++++++++++++ src/cas_parser/_utils/_transform.py | 6 +- src/cas_parser/_utils/_typing.py | 2 +- src/cas_parser/_utils/_utils.py | 1 - tests/test_models.py | 48 ++++---- tests/test_transform.py | 16 +-- tests/test_utils/test_datetime_parse.py | 110 ++++++++++++++++++ tests/utils.py | 8 +- 13 files changed, 432 insertions(+), 132 deletions(-) create mode 100644 src/cas_parser/_utils/_compat.py create mode 100644 src/cas_parser/_utils/_datetime_parse.py create mode 100644 tests/test_utils/test_datetime_parse.py diff --git a/src/cas_parser/_base_client.py b/src/cas_parser/_base_client.py index c41565d..8a47ab7 100644 --- a/src/cas_parser/_base_client.py +++ b/src/cas_parser/_base_client.py @@ -59,7 +59,7 @@ ModelBuilderProtocol, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping -from ._compat import PYDANTIC_V2, model_copy, model_dump +from ._compat import PYDANTIC_V1, model_copy, model_dump from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, @@ -232,7 +232,7 @@ def _set_private_attributes( model: Type[_T], options: FinalRequestOptions, ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: self.__pydantic_private__ = {} self._model = model @@ -320,7 +320,7 @@ def _set_private_attributes( client: AsyncAPIClient, options: FinalRequestOptions, ) -> None: - if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: self.__pydantic_private__ = {} self._model = model diff --git a/src/cas_parser/_compat.py b/src/cas_parser/_compat.py index 92d9ee6..bdef67f 100644 --- a/src/cas_parser/_compat.py +++ b/src/cas_parser/_compat.py @@ -12,14 +12,13 @@ _T = TypeVar("_T") _ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) -# --------------- Pydantic v2 compatibility --------------- +# --------------- Pydantic v2, v3 compatibility --------------- # Pyright incorrectly reports some of our functions as overriding a method when they don't # pyright: reportIncompatibleMethodOverride=false -PYDANTIC_V2 = pydantic.VERSION.startswith("2.") +PYDANTIC_V1 = pydantic.VERSION.startswith("1.") -# v1 re-exports if TYPE_CHECKING: def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 @@ -44,90 +43,92 @@ def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 ... else: - if PYDANTIC_V2: - from pydantic.v1.typing import ( + # v1 re-exports + if PYDANTIC_V1: + from pydantic.typing import ( get_args as get_args, is_union as is_union, get_origin as get_origin, is_typeddict as is_typeddict, is_literal_type as is_literal_type, ) - from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime else: - from pydantic.typing import ( + from ._utils import ( get_args as get_args, is_union as is_union, get_origin as get_origin, + parse_date as parse_date, is_typeddict as is_typeddict, + parse_datetime as parse_datetime, is_literal_type as is_literal_type, ) - from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime # refactored config if TYPE_CHECKING: from pydantic import ConfigDict as ConfigDict else: - if PYDANTIC_V2: - from pydantic import ConfigDict - else: + if PYDANTIC_V1: # TODO: provide an error message here? ConfigDict = None + else: + from pydantic import ConfigDict as ConfigDict # renamed methods / properties def parse_obj(model: type[_ModelT], value: object) -> _ModelT: - if PYDANTIC_V2: - return model.model_validate(value) - else: + if PYDANTIC_V1: return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + else: + return model.model_validate(value) def field_is_required(field: FieldInfo) -> bool: - if PYDANTIC_V2: - return field.is_required() - return field.required # type: ignore + if PYDANTIC_V1: + return field.required # type: ignore + return field.is_required() def field_get_default(field: FieldInfo) -> Any: value = field.get_default() - if PYDANTIC_V2: - from pydantic_core import PydanticUndefined - - if value == PydanticUndefined: - return None + if PYDANTIC_V1: return value + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None return value def field_outer_type(field: FieldInfo) -> Any: - if PYDANTIC_V2: - return field.annotation - return field.outer_type_ # type: ignore + if PYDANTIC_V1: + return field.outer_type_ # type: ignore + return field.annotation def get_model_config(model: type[pydantic.BaseModel]) -> Any: - if PYDANTIC_V2: - return model.model_config - return model.__config__ # type: ignore + if PYDANTIC_V1: + return model.__config__ # type: ignore + return model.model_config def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: - if PYDANTIC_V2: - return model.model_fields - return model.__fields__ # type: ignore + if PYDANTIC_V1: + return model.__fields__ # type: ignore + return model.model_fields def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: - if PYDANTIC_V2: - return model.model_copy(deep=deep) - return model.copy(deep=deep) # type: ignore + if PYDANTIC_V1: + return model.copy(deep=deep) # type: ignore + return model.model_copy(deep=deep) def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: - if PYDANTIC_V2: - return model.model_dump_json(indent=indent) - return model.json(indent=indent) # type: ignore + if PYDANTIC_V1: + return model.json(indent=indent) # type: ignore + return model.model_dump_json(indent=indent) def model_dump( @@ -139,14 +140,14 @@ def model_dump( warnings: bool = True, mode: Literal["json", "python"] = "python", ) -> dict[str, Any]: - if PYDANTIC_V2 or hasattr(model, "model_dump"): + if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( mode=mode, exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 - warnings=warnings if PYDANTIC_V2 else True, + warnings=True if PYDANTIC_V1 else warnings, ) return cast( "dict[str, Any]", @@ -159,9 +160,9 @@ def model_dump( def model_parse(model: type[_ModelT], data: Any) -> _ModelT: - if PYDANTIC_V2: - return model.model_validate(data) - return model.parse_obj(data) # pyright: ignore[reportDeprecated] + if PYDANTIC_V1: + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + return model.model_validate(data) # generic models @@ -170,17 +171,16 @@ def model_parse(model: type[_ModelT], data: Any) -> _ModelT: class GenericModel(pydantic.BaseModel): ... else: - if PYDANTIC_V2: + if PYDANTIC_V1: + import pydantic.generics + + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... + else: # there no longer needs to be a distinction in v2 but # we still have to create our own subclass to avoid # inconsistent MRO ordering errors class GenericModel(pydantic.BaseModel): ... - else: - import pydantic.generics - - class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... - # cached properties if TYPE_CHECKING: diff --git a/src/cas_parser/_models.py b/src/cas_parser/_models.py index 92f7c10..3a6017e 100644 --- a/src/cas_parser/_models.py +++ b/src/cas_parser/_models.py @@ -50,7 +50,7 @@ strip_annotated_type, ) from ._compat import ( - PYDANTIC_V2, + PYDANTIC_V1, ConfigDict, GenericModel as BaseGenericModel, get_args, @@ -81,11 +81,7 @@ class _ConfigProtocol(Protocol): class BaseModel(pydantic.BaseModel): - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict( - extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) - ) - else: + if PYDANTIC_V1: @property @override @@ -95,6 +91,10 @@ def model_fields_set(self) -> set[str]: class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] extra: Any = pydantic.Extra.allow # type: ignore + else: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) def to_dict( self, @@ -215,25 +215,25 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] if key not in model_fields: parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value - if PYDANTIC_V2: - _extra[key] = parsed - else: + if PYDANTIC_V1: _fields_set.add(key) fields_values[key] = parsed + else: + _extra[key] = parsed object.__setattr__(m, "__dict__", fields_values) - if PYDANTIC_V2: - # these properties are copied from Pydantic's `model_construct()` method - object.__setattr__(m, "__pydantic_private__", None) - object.__setattr__(m, "__pydantic_extra__", _extra) - object.__setattr__(m, "__pydantic_fields_set__", _fields_set) - else: + if PYDANTIC_V1: # init_private_attributes() does not exist in v2 m._init_private_attributes() # type: ignore # copied from Pydantic v1's `construct()` method object.__setattr__(m, "__fields_set__", _fields_set) + else: + # these properties are copied from Pydantic's `model_construct()` method + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", _extra) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) return m @@ -243,7 +243,7 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] # although not in practice model_construct = construct - if not PYDANTIC_V2: + if PYDANTIC_V1: # we define aliases for some of the new pydantic v2 methods so # that we can just document these methods without having to specify # a specific pydantic version as some users may not know which @@ -363,10 +363,10 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) - if PYDANTIC_V2: - type_ = field.annotation - else: + if PYDANTIC_V1: type_ = cast(type, field.outer_type_) # type: ignore + else: + type_ = field.annotation # type: ignore if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") @@ -375,7 +375,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: - if not PYDANTIC_V2: + if PYDANTIC_V1: # TODO return None @@ -628,30 +628,30 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, for variant in get_args(union): variant = strip_annotated_type(variant) if is_basemodel_type(variant): - if PYDANTIC_V2: - field = _extract_field_schema_pv2(variant, discriminator_field_name) - if not field: + if PYDANTIC_V1: + field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + if not field_info: continue # Note: if one variant defines an alias then they all should - discriminator_alias = field.get("serialization_alias") - - field_schema = field["schema"] + discriminator_alias = field_info.alias - if field_schema["type"] == "literal": - for entry in cast("LiteralSchema", field_schema)["expected"]: + if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): + for entry in get_args(annotation): if isinstance(entry, str): mapping[entry] = variant else: - field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - if not field_info: + field = _extract_field_schema_pv2(variant, discriminator_field_name) + if not field: continue # Note: if one variant defines an alias then they all should - discriminator_alias = field_info.alias + discriminator_alias = field.get("serialization_alias") - if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): - for entry in get_args(annotation): + field_schema = field["schema"] + + if field_schema["type"] == "literal": + for entry in cast("LiteralSchema", field_schema)["expected"]: if isinstance(entry, str): mapping[entry] = variant @@ -714,7 +714,7 @@ class GenericModel(BaseGenericModel, BaseModel): pass -if PYDANTIC_V2: +if not PYDANTIC_V1: from pydantic import TypeAdapter as _TypeAdapter _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) @@ -782,12 +782,12 @@ class FinalRequestOptions(pydantic.BaseModel): json_data: Union[Body, None] = None extra_json: Union[AnyMapping, None] = None - if PYDANTIC_V2: - model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) - else: + if PYDANTIC_V1: class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] arbitrary_types_allowed: bool = True + else: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) def get_max_retries(self, max_retries: int) -> int: if isinstance(self.max_retries, NotGiven): @@ -820,9 +820,9 @@ def construct( # type: ignore key: strip_not_given(value) for key, value in values.items() } - if PYDANTIC_V2: - return super().model_construct(_fields_set, **kwargs) - return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + if PYDANTIC_V1: + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + return super().model_construct(_fields_set, **kwargs) if not TYPE_CHECKING: # type checkers incorrectly complain about this assignment diff --git a/src/cas_parser/_utils/__init__.py b/src/cas_parser/_utils/__init__.py index ca547ce..dc64e29 100644 --- a/src/cas_parser/_utils/__init__.py +++ b/src/cas_parser/_utils/__init__.py @@ -10,7 +10,6 @@ lru_cache as lru_cache, is_mapping as is_mapping, is_tuple_t as is_tuple_t, - parse_date as parse_date, is_iterable as is_iterable, is_sequence as is_sequence, coerce_float as coerce_float, @@ -23,7 +22,6 @@ coerce_boolean as coerce_boolean, coerce_integer as coerce_integer, file_from_path as file_from_path, - parse_datetime as parse_datetime, strip_not_given as strip_not_given, deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, @@ -32,6 +30,13 @@ maybe_coerce_boolean as maybe_coerce_boolean, maybe_coerce_integer as maybe_coerce_integer, ) +from ._compat import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, +) from ._typing import ( is_list_type as is_list_type, is_union_type as is_union_type, @@ -56,3 +61,4 @@ function_has_argument as function_has_argument, assert_signatures_in_sync as assert_signatures_in_sync, ) +from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime diff --git a/src/cas_parser/_utils/_compat.py b/src/cas_parser/_utils/_compat.py new file mode 100644 index 0000000..dd70323 --- /dev/null +++ b/src/cas_parser/_utils/_compat.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import sys +import typing_extensions +from typing import Any, Type, Union, Literal, Optional +from datetime import date, datetime +from typing_extensions import get_args as _get_args, get_origin as _get_origin + +from .._types import StrBytesIntFloat +from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime + +_LITERAL_TYPES = {Literal, typing_extensions.Literal} + + +def get_args(tp: type[Any]) -> tuple[Any, ...]: + return _get_args(tp) + + +def get_origin(tp: type[Any]) -> type[Any] | None: + return _get_origin(tp) + + +def is_union(tp: Optional[Type[Any]]) -> bool: + if sys.version_info < (3, 10): + return tp is Union # type: ignore[comparison-overlap] + else: + import types + + return tp is Union or tp is types.UnionType + + +def is_typeddict(tp: Type[Any]) -> bool: + return typing_extensions.is_typeddict(tp) + + +def is_literal_type(tp: Type[Any]) -> bool: + return get_origin(tp) in _LITERAL_TYPES + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + return _parse_date(value) + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + return _parse_datetime(value) diff --git a/src/cas_parser/_utils/_datetime_parse.py b/src/cas_parser/_utils/_datetime_parse.py new file mode 100644 index 0000000..7cb9d9e --- /dev/null +++ b/src/cas_parser/_utils/_datetime_parse.py @@ -0,0 +1,136 @@ +""" +This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py +without the Pydantic v1 specific errors. +""" + +from __future__ import annotations + +import re +from typing import Dict, Union, Optional +from datetime import date, datetime, timezone, timedelta + +from .._types import StrBytesIntFloat + +date_expr = r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})" +time_expr = ( + r"(?P\d{1,2}):(?P\d{1,2})" + r"(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?" + r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$" +) + +date_re = re.compile(f"{date_expr}$") +datetime_re = re.compile(f"{date_expr}[T ]{time_expr}") + + +EPOCH = datetime(1970, 1, 1) +# if greater than this, the number is in ms, if less than or equal it's in seconds +# (in seconds this is 11th October 2603, in ms it's 20th August 1970) +MS_WATERSHED = int(2e10) +# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9 +MAX_NUMBER = int(3e20) + + +def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]: + if isinstance(value, (int, float)): + return value + try: + return float(value) + except ValueError: + return None + except TypeError: + raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None + + +def _from_unix_seconds(seconds: Union[int, float]) -> datetime: + if seconds > MAX_NUMBER: + return datetime.max + elif seconds < -MAX_NUMBER: + return datetime.min + + while abs(seconds) > MS_WATERSHED: + seconds /= 1000 + dt = EPOCH + timedelta(seconds=seconds) + return dt.replace(tzinfo=timezone.utc) + + +def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]: + if value == "Z": + return timezone.utc + elif value is not None: + offset_mins = int(value[-2:]) if len(value) > 3 else 0 + offset = 60 * int(value[1:3]) + offset_mins + if value[0] == "-": + offset = -offset + return timezone(timedelta(minutes=offset)) + else: + return None + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + """ + Parse a datetime/int/float/string and return a datetime.datetime. + + This function supports time zone offsets. When the input contains one, + the output uses a timezone with a fixed offset from UTC. + + Raise ValueError if the input is well formatted but not a valid datetime. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, datetime): + return value + + number = _get_numeric(value, "datetime") + if number is not None: + return _from_unix_seconds(number) + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + + match = datetime_re.match(value) + if match is None: + raise ValueError("invalid datetime format") + + kw = match.groupdict() + if kw["microsecond"]: + kw["microsecond"] = kw["microsecond"].ljust(6, "0") + + tzinfo = _parse_timezone(kw.pop("tzinfo")) + kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None} + kw_["tzinfo"] = tzinfo + + return datetime(**kw_) # type: ignore + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + """ + Parse a date/int/float/string and return a datetime.date. + + Raise ValueError if the input is well formatted but not a valid date. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, date): + if isinstance(value, datetime): + return value.date() + else: + return value + + number = _get_numeric(value, "date") + if number is not None: + return _from_unix_seconds(number).date() + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + match = date_re.match(value) + if match is None: + raise ValueError("invalid date format") + + kw = {k: int(v) for k, v in match.groupdict().items()} + + try: + return date(**kw) + except ValueError: + raise ValueError("invalid date format") from None diff --git a/src/cas_parser/_utils/_transform.py b/src/cas_parser/_utils/_transform.py index f0bcefd..c19124f 100644 --- a/src/cas_parser/_utils/_transform.py +++ b/src/cas_parser/_utils/_transform.py @@ -19,6 +19,7 @@ is_sequence, ) from .._files import is_base64_file_input +from ._compat import get_origin, is_typeddict from ._typing import ( is_list_type, is_union_type, @@ -29,7 +30,6 @@ is_annotated_type, strip_annotated_type, ) -from .._compat import get_origin, model_dump, is_typeddict _T = TypeVar("_T") @@ -169,6 +169,8 @@ def _transform_recursive( Defaults to the same value as the `annotation` argument. """ + from .._compat import model_dump + if inner_type is None: inner_type = annotation @@ -333,6 +335,8 @@ async def _async_transform_recursive( Defaults to the same value as the `annotation` argument. """ + from .._compat import model_dump + if inner_type is None: inner_type = annotation diff --git a/src/cas_parser/_utils/_typing.py b/src/cas_parser/_utils/_typing.py index 845cd6b..193109f 100644 --- a/src/cas_parser/_utils/_typing.py +++ b/src/cas_parser/_utils/_typing.py @@ -15,7 +15,7 @@ from ._utils import lru_cache from .._types import InheritsGeneric -from .._compat import is_union as _is_union +from ._compat import is_union as _is_union def is_annotated_type(typ: type) -> bool: diff --git a/src/cas_parser/_utils/_utils.py b/src/cas_parser/_utils/_utils.py index ea3cf3f..f081859 100644 --- a/src/cas_parser/_utils/_utils.py +++ b/src/cas_parser/_utils/_utils.py @@ -22,7 +22,6 @@ import sniffio from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike -from .._compat import parse_date as parse_date, parse_datetime as parse_datetime _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) diff --git a/tests/test_models.py b/tests/test_models.py index 2a79cf8..ffd0d05 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -8,7 +8,7 @@ from pydantic import Field from cas_parser._utils import PropertyInfo -from cas_parser._compat import PYDANTIC_V2, parse_obj, model_dump, model_json +from cas_parser._compat import PYDANTIC_V1, parse_obj, model_dump, model_json from cas_parser._models import BaseModel, construct_type @@ -294,12 +294,12 @@ class Model(BaseModel): assert cast(bool, m.foo) is True m = Model.construct(foo={"name": 3}) - if PYDANTIC_V2: - assert isinstance(m.foo, Submodel1) - assert m.foo.name == 3 # type: ignore - else: + if PYDANTIC_V1: assert isinstance(m.foo, Submodel2) assert m.foo.name == "3" + else: + assert isinstance(m.foo, Submodel1) + assert m.foo.name == 3 # type: ignore def test_list_of_unions() -> None: @@ -426,10 +426,10 @@ class Model(BaseModel): expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) - if PYDANTIC_V2: - expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' - else: + if PYDANTIC_V1: expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' + else: + expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' model = Model.construct(created_at="2019-12-27T18:11:19.117Z") assert model.created_at == expected @@ -531,7 +531,7 @@ class Model2(BaseModel): assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} assert m4.to_dict(mode="json") == {"created_at": time_str} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_dict(warnings=False) @@ -556,7 +556,7 @@ class Model(BaseModel): assert m3.model_dump() == {"foo": None} assert m3.model_dump(exclude_none=True) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump(round_trip=True) @@ -580,10 +580,10 @@ class Model(BaseModel): assert json.loads(m.to_json()) == {"FOO": "hello"} assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} - if PYDANTIC_V2: - assert m.to_json(indent=None) == '{"FOO":"hello"}' - else: + if PYDANTIC_V1: assert m.to_json(indent=None) == '{"FOO": "hello"}' + else: + assert m.to_json(indent=None) == '{"FOO":"hello"}' m2 = Model() assert json.loads(m2.to_json()) == {} @@ -595,7 +595,7 @@ class Model(BaseModel): assert json.loads(m3.to_json()) == {"FOO": None} assert json.loads(m3.to_json(exclude_none=True)) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_json(warnings=False) @@ -622,7 +622,7 @@ class Model(BaseModel): assert json.loads(m3.model_dump_json()) == {"foo": None} assert json.loads(m3.model_dump_json(exclude_none=True)) == {} - if not PYDANTIC_V2: + if PYDANTIC_V1: with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump_json(round_trip=True) @@ -679,12 +679,12 @@ class B(BaseModel): ) assert isinstance(m, A) assert m.type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: + if PYDANTIC_V1: # pydantic v1 automatically converts inputs to strings # if the expected type is a str assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] def test_discriminated_unions_unknown_variant() -> None: @@ -768,12 +768,12 @@ class B(BaseModel): ) assert isinstance(m, A) assert m.foo_type == "a" - if PYDANTIC_V2: - assert m.data == 100 # type: ignore[comparison-overlap] - else: + if PYDANTIC_V1: # pydantic v1 automatically converts inputs to strings # if the expected type is a str assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None: @@ -833,7 +833,7 @@ class B(BaseModel): assert UnionType.__discriminator__ is discriminator -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") def test_type_alias_type() -> None: Alias = TypeAliasType("Alias", str) # pyright: ignore @@ -849,7 +849,7 @@ class Model(BaseModel): assert m.union == "bar" -@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") def test_field_named_cls() -> None: class Model(BaseModel): cls: str @@ -936,7 +936,7 @@ class Type2(BaseModel): assert isinstance(model.value, InnerType2) -@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now") +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2 for now") def test_extra_properties() -> None: class Item(BaseModel): prop: int diff --git a/tests/test_transform.py b/tests/test_transform.py index 5504d5e..ce97c84 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -15,7 +15,7 @@ parse_datetime, async_transform as _async_transform, ) -from cas_parser._compat import PYDANTIC_V2 +from cas_parser._compat import PYDANTIC_V1 from cas_parser._models import BaseModel _T = TypeVar("_T") @@ -189,7 +189,7 @@ class DateModel(BaseModel): @pytest.mark.asyncio async def test_iso8601_format(use_async: bool) -> None: dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - tz = "Z" if PYDANTIC_V2 else "+00:00" + tz = "+00:00" if PYDANTIC_V1 else "Z" assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] @@ -297,11 +297,11 @@ async def test_pydantic_unknown_field(use_async: bool) -> None: @pytest.mark.asyncio async def test_pydantic_mismatched_types(use_async: bool) -> None: model = MyModel.construct(foo=True) - if PYDANTIC_V2: + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: with pytest.warns(UserWarning): params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) assert cast(Any, params) == {"foo": True} @@ -309,11 +309,11 @@ async def test_pydantic_mismatched_types(use_async: bool) -> None: @pytest.mark.asyncio async def test_pydantic_mismatched_object_type(use_async: bool) -> None: model = MyModel.construct(foo=MyModel.construct(hello="world")) - if PYDANTIC_V2: + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: with pytest.warns(UserWarning): params = await transform(model, Any, use_async) - else: - params = await transform(model, Any, use_async) assert cast(Any, params) == {"foo": {"hello": "world"}} diff --git a/tests/test_utils/test_datetime_parse.py b/tests/test_utils/test_datetime_parse.py new file mode 100644 index 0000000..2e4ce4a --- /dev/null +++ b/tests/test_utils/test_datetime_parse.py @@ -0,0 +1,110 @@ +""" +Copied from https://github.com/pydantic/pydantic/blob/v1.10.22/tests/test_datetime_parse.py +with modifications so it works without pydantic v1 imports. +""" + +from typing import Type, Union +from datetime import date, datetime, timezone, timedelta + +import pytest + +from cas_parser._utils import parse_date, parse_datetime + + +def create_tz(minutes: int) -> timezone: + return timezone(timedelta(minutes=minutes)) + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + ("1494012444.883309", date(2017, 5, 5)), + (b"1494012444.883309", date(2017, 5, 5)), + (1_494_012_444.883_309, date(2017, 5, 5)), + ("1494012444", date(2017, 5, 5)), + (1_494_012_444, date(2017, 5, 5)), + (0, date(1970, 1, 1)), + ("2012-04-23", date(2012, 4, 23)), + (b"2012-04-23", date(2012, 4, 23)), + ("2012-4-9", date(2012, 4, 9)), + (date(2012, 4, 9), date(2012, 4, 9)), + (datetime(2012, 4, 9, 12, 15), date(2012, 4, 9)), + # Invalid inputs + ("x20120423", ValueError), + ("2012-04-56", ValueError), + (19_999_999_999, date(2603, 10, 11)), # just before watershed + (20_000_000_001, date(1970, 8, 20)), # just after watershed + (1_549_316_052, date(2019, 2, 4)), # nowish in s + (1_549_316_052_104, date(2019, 2, 4)), # nowish in ms + (1_549_316_052_104_324, date(2019, 2, 4)), # nowish in μs + (1_549_316_052_104_324_096, date(2019, 2, 4)), # nowish in ns + ("infinity", date(9999, 12, 31)), + ("inf", date(9999, 12, 31)), + (float("inf"), date(9999, 12, 31)), + ("infinity ", date(9999, 12, 31)), + (int("1" + "0" * 100), date(9999, 12, 31)), + (1e1000, date(9999, 12, 31)), + ("-infinity", date(1, 1, 1)), + ("-inf", date(1, 1, 1)), + ("nan", ValueError), + ], +) +def test_date_parsing(value: Union[str, bytes, int, float], result: Union[date, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_date(value) + else: + assert parse_date(value) == result + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + # values in seconds + ("1494012444.883309", datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + (1_494_012_444.883_309, datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + ("1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (b"1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (1_494_012_444, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + # values in ms + ("1494012444000.883309", datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)), + ("-1494012444000.883309", datetime(1922, 8, 29, 4, 32, 35, 999117, tzinfo=timezone.utc)), + (1_494_012_444_000, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + ("2012-04-23T09:15:00", datetime(2012, 4, 23, 9, 15)), + ("2012-4-9 4:8:16", datetime(2012, 4, 9, 4, 8, 16)), + ("2012-04-23T09:15:00Z", datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)), + ("2012-4-9 4:8:16-0320", datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))), + ("2012-04-23T10:20:30.400+02:30", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(150))), + ("2012-04-23T10:20:30.400+02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(120))), + ("2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (b"2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (datetime(2017, 5, 5), datetime(2017, 5, 5)), + (0, datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)), + # Invalid inputs + ("x20120423091500", ValueError), + ("2012-04-56T09:15:90", ValueError), + ("2012-04-23T11:05:00-25:00", ValueError), + (19_999_999_999, datetime(2603, 10, 11, 11, 33, 19, tzinfo=timezone.utc)), # just before watershed + (20_000_000_001, datetime(1970, 8, 20, 11, 33, 20, 1000, tzinfo=timezone.utc)), # just after watershed + (1_549_316_052, datetime(2019, 2, 4, 21, 34, 12, 0, tzinfo=timezone.utc)), # nowish in s + (1_549_316_052_104, datetime(2019, 2, 4, 21, 34, 12, 104_000, tzinfo=timezone.utc)), # nowish in ms + (1_549_316_052_104_324, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in μs + (1_549_316_052_104_324_096, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in ns + ("infinity", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf ", datetime(9999, 12, 31, 23, 59, 59, 999999)), + (1e50, datetime(9999, 12, 31, 23, 59, 59, 999999)), + (float("inf"), datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("-infinity", datetime(1, 1, 1, 0, 0)), + ("-inf", datetime(1, 1, 1, 0, 0)), + ("nan", ValueError), + ], +) +def test_datetime_parsing(value: Union[str, bytes, int, float], result: Union[datetime, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_datetime(value) + else: + assert parse_datetime(value) == result diff --git a/tests/utils.py b/tests/utils.py index 4df38ad..233622e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -19,7 +19,7 @@ is_annotated_type, is_type_alias_type, ) -from cas_parser._compat import PYDANTIC_V2, field_outer_type, get_model_fields +from cas_parser._compat import PYDANTIC_V1, field_outer_type, get_model_fields from cas_parser._models import BaseModel BaseModelT = TypeVar("BaseModelT", bound=BaseModel) @@ -28,12 +28,12 @@ def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: for name, field in get_model_fields(model).items(): field_value = getattr(value, name) - if PYDANTIC_V2: - allow_none = False - else: + if PYDANTIC_V1: # in v1 nullability was structured differently # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields allow_none = getattr(field, "allow_none", False) + else: + allow_none = False assert_matches_type( field_outer_type(field), From b2f5e0889b728a0782bda397974dbe1d377f10b0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 03:08:58 +0000 Subject: [PATCH 022/116] chore(internal): move mypy configurations to `pyproject.toml` file --- mypy.ini | 50 ------------------------------------------------ pyproject.toml | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 50 deletions(-) delete mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 14b5020..0000000 --- a/mypy.ini +++ /dev/null @@ -1,50 +0,0 @@ -[mypy] -pretty = True -show_error_codes = True - -# Exclude _files.py because mypy isn't smart enough to apply -# the correct type narrowing and as this is an internal module -# it's fine to just use Pyright. -# -# We also exclude our `tests` as mypy doesn't always infer -# types correctly and Pyright will still catch any type errors. -exclude = ^(src/cas_parser/_files\.py|_dev/.*\.py|tests/.*)$ - -strict_equality = True -implicit_reexport = True -check_untyped_defs = True -no_implicit_optional = True - -warn_return_any = True -warn_unreachable = True -warn_unused_configs = True - -# Turn these options off as it could cause conflicts -# with the Pyright options. -warn_unused_ignores = False -warn_redundant_casts = False - -disallow_any_generics = True -disallow_untyped_defs = True -disallow_untyped_calls = True -disallow_subclassing_any = True -disallow_incomplete_defs = True -disallow_untyped_decorators = True -cache_fine_grained = True - -# By default, mypy reports an error if you assign a value to the result -# of a function call that doesn't return anything. We do this in our test -# cases: -# ``` -# result = ... -# assert result is None -# ``` -# Changing this codegen to make mypy happy would increase complexity -# and would not be worth it. -disable_error_code = func-returns-value,overload-cannot-match - -# https://github.com/python/mypy/issues/12162 -[mypy.overrides] -module = "black.files.*" -ignore_errors = true -ignore_missing_imports = true diff --git a/pyproject.toml b/pyproject.toml index 3ddf740..74c7ddc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,6 +157,58 @@ reportOverlappingOverload = false reportImportCycles = false reportPrivateUsage = false +[tool.mypy] +pretty = true +show_error_codes = true + +# Exclude _files.py because mypy isn't smart enough to apply +# the correct type narrowing and as this is an internal module +# it's fine to just use Pyright. +# +# We also exclude our `tests` as mypy doesn't always infer +# types correctly and Pyright will still catch any type errors. +exclude = ['src/cas_parser/_files.py', '_dev/.*.py', 'tests/.*'] + +strict_equality = true +implicit_reexport = true +check_untyped_defs = true +no_implicit_optional = true + +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true + +# Turn these options off as it could cause conflicts +# with the Pyright options. +warn_unused_ignores = false +warn_redundant_casts = false + +disallow_any_generics = true +disallow_untyped_defs = true +disallow_untyped_calls = true +disallow_subclassing_any = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +cache_fine_grained = true + +# By default, mypy reports an error if you assign a value to the result +# of a function call that doesn't return anything. We do this in our test +# cases: +# ``` +# result = ... +# assert result is None +# ``` +# Changing this codegen to make mypy happy would increase complexity +# and would not be worth it. +disable_error_code = "func-returns-value,overload-cannot-match" + +# https://github.com/python/mypy/issues/12162 +[[tool.mypy.overrides]] +module = "black.files.*" +ignore_errors = true +ignore_missing_imports = true + + [tool.ruff] line-length = 120 output-format = "grouped" From 2e087d7cee602e26cb3a585fba78f8e184b7f82a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 6 Sep 2025 03:27:29 +0000 Subject: [PATCH 023/116] chore(tests): simplify `get_platform` test `nest_asyncio` is archived and broken on some platforms so it's not worth keeping in our test suite. --- pyproject.toml | 1 - requirements-dev.lock | 1 - tests/test_client.py | 53 +++++-------------------------------------- 3 files changed, 6 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 74c7ddc..386d45c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,6 @@ dev-dependencies = [ "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", - "nest_asyncio==1.6.0", "pytest-xdist>=3.6.1", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index 2625645..d000467 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -75,7 +75,6 @@ multidict==6.4.4 mypy==1.14.1 mypy-extensions==1.0.0 # via mypy -nest-asyncio==1.6.0 nodeenv==1.8.0 # via pyright nox==2023.4.22 diff --git a/tests/test_client.py b/tests/test_client.py index 201c609..e5b787e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,13 +6,10 @@ import os import sys import json -import time import asyncio import inspect -import subprocess import tracemalloc from typing import Any, Union, cast -from textwrap import dedent from unittest import mock from typing_extensions import Literal @@ -23,14 +20,17 @@ from cas_parser import CasParser, AsyncCasParser, APIResponseValidationError from cas_parser._types import Omit +from cas_parser._utils import asyncify from cas_parser._models import BaseModel, FinalRequestOptions from cas_parser._exceptions import APIStatusError, CasParserError, APITimeoutError, APIResponseValidationError from cas_parser._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, + OtherPlatform, DefaultHttpxClient, DefaultAsyncHttpxClient, + get_platform, make_request_options, ) @@ -1633,50 +1633,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" - def test_get_platform(self) -> None: - # A previous implementation of asyncify could leave threads unterminated when - # used with nest_asyncio. - # - # Since nest_asyncio.apply() is global and cannot be un-applied, this - # test is run in a separate process to avoid affecting other tests. - test_code = dedent(""" - import asyncio - import nest_asyncio - import threading - - from cas_parser._utils import asyncify - from cas_parser._base_client import get_platform - - async def test_main() -> None: - result = await asyncify(get_platform)() - print(result) - for thread in threading.enumerate(): - print(thread.name) - - nest_asyncio.apply() - asyncio.run(test_main()) - """) - with subprocess.Popen( - [sys.executable, "-c", test_code], - text=True, - ) as process: - timeout = 10 # seconds - - start_time = time.monotonic() - while True: - return_code = process.poll() - if return_code is not None: - if return_code != 0: - raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") - - # success - break - - if time.monotonic() - start_time > timeout: - process.kill() - raise AssertionError("calling get_platform using asyncify resulted in a hung process") - - time.sleep(0.1) + async def test_get_platform(self) -> None: + platform = await asyncify(get_platform)() + assert isinstance(platform, (str, OtherPlatform)) async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly From 49f65c4b1c34a3ef12ee9bc9144dcfa50c8f7b4d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 12:24:06 +0000 Subject: [PATCH 024/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 06d6df2..2601677 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.2" + ".": "1.1.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 386d45c..33ccf0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.0.2" +version = "1.1.0" description = "The official Python library for the CAS Parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 383444a..69821a2 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.0.2" # x-release-please-version +__version__ = "1.1.0" # x-release-please-version From 81d833d23c6ffa1d2f92116dac3e3cdfa5476151 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 02:25:50 +0000 Subject: [PATCH 025/116] chore(internal): update pydantic dependency --- requirements-dev.lock | 7 +++++-- requirements.lock | 7 +++++-- src/cas_parser/_models.py | 14 ++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index d000467..cd93fa9 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -88,9 +88,9 @@ pluggy==1.5.0 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.10.3 +pydantic==2.11.9 # via cas-parser-python -pydantic-core==2.27.1 +pydantic-core==2.33.2 # via pydantic pygments==2.18.0 # via rich @@ -126,6 +126,9 @@ typing-extensions==4.12.2 # via pydantic # via pydantic-core # via pyright + # via typing-inspection +typing-inspection==0.4.1 + # via pydantic virtualenv==20.24.5 # via nox yarl==1.20.0 diff --git a/requirements.lock b/requirements.lock index 46d36df..c02016b 100644 --- a/requirements.lock +++ b/requirements.lock @@ -55,9 +55,9 @@ multidict==6.4.4 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.10.3 +pydantic==2.11.9 # via cas-parser-python -pydantic-core==2.27.1 +pydantic-core==2.33.2 # via pydantic sniffio==1.3.0 # via anyio @@ -68,5 +68,8 @@ typing-extensions==4.12.2 # via multidict # via pydantic # via pydantic-core + # via typing-inspection +typing-inspection==0.4.1 + # via pydantic yarl==1.20.0 # via aiohttp diff --git a/src/cas_parser/_models.py b/src/cas_parser/_models.py index 3a6017e..6a3cd1d 100644 --- a/src/cas_parser/_models.py +++ b/src/cas_parser/_models.py @@ -256,7 +256,7 @@ def model_dump( mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, - by_alias: bool = False, + by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, @@ -264,6 +264,7 @@ def model_dump( warnings: bool | Literal["none", "warn", "error"] = True, context: dict[str, Any] | None = None, serialize_as_any: bool = False, + fallback: Callable[[Any], Any] | None = None, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -295,10 +296,12 @@ def model_dump( raise ValueError("context is only supported in Pydantic v2") if serialize_as_any != False: raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, - by_alias=by_alias, + by_alias=by_alias if by_alias is not None else False, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, @@ -313,13 +316,14 @@ def model_dump_json( indent: int | None = None, include: IncEx | None = None, exclude: IncEx | None = None, - by_alias: bool = False, + by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, context: dict[str, Any] | None = None, + fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json @@ -348,11 +352,13 @@ def model_dump_json( raise ValueError("context is only supported in Pydantic v2") if serialize_as_any != False: raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, exclude=exclude, - by_alias=by_alias, + by_alias=by_alias if by_alias is not None else False, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, From 52d022653dfaf2cd365df40e4c38611562164c69 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 02:29:50 +0000 Subject: [PATCH 026/116] chore(types): change optional parameter type from NotGiven to Omit --- src/cas_parser/__init__.py | 4 +- src/cas_parser/_base_client.py | 18 +++---- src/cas_parser/_client.py | 16 +++--- src/cas_parser/_qs.py | 14 ++--- src/cas_parser/_types.py | 29 ++++++---- src/cas_parser/_utils/_transform.py | 4 +- src/cas_parser/_utils/_utils.py | 8 +-- src/cas_parser/resources/cas_generator.py | 14 ++--- src/cas_parser/resources/cas_parser.py | 66 +++++++++++------------ tests/test_transform.py | 11 +++- 10 files changed, 100 insertions(+), 84 deletions(-) diff --git a/src/cas_parser/__init__.py b/src/cas_parser/__init__.py index a6c342f..1e1d246 100644 --- a/src/cas_parser/__init__.py +++ b/src/cas_parser/__init__.py @@ -3,7 +3,7 @@ import typing as _t from . import types -from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given from ._utils import file_from_path from ._client import ( Client, @@ -48,7 +48,9 @@ "ProxiesTypes", "NotGiven", "NOT_GIVEN", + "not_given", "Omit", + "omit", "CasParserError", "APIError", "APIStatusError", diff --git a/src/cas_parser/_base_client.py b/src/cas_parser/_base_client.py index 8a47ab7..fc89c5a 100644 --- a/src/cas_parser/_base_client.py +++ b/src/cas_parser/_base_client.py @@ -42,7 +42,6 @@ from ._qs import Querystring from ._files import to_httpx_files, async_to_httpx_files from ._types import ( - NOT_GIVEN, Body, Omit, Query, @@ -57,6 +56,7 @@ RequestOptions, HttpxRequestFiles, ModelBuilderProtocol, + not_given, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping from ._compat import PYDANTIC_V1, model_copy, model_dump @@ -145,9 +145,9 @@ def __init__( def __init__( self, *, - url: URL | NotGiven = NOT_GIVEN, - json: Body | NotGiven = NOT_GIVEN, - params: Query | NotGiven = NOT_GIVEN, + url: URL | NotGiven = not_given, + json: Body | NotGiven = not_given, + params: Query | NotGiven = not_given, ) -> None: self.url = url self.json = json @@ -595,7 +595,7 @@ def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalReques # we internally support defining a temporary header to override the # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` # see _response.py for implementation details - override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN) + override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, not_given) if is_given(override_cast_to): options.headers = headers return cast(Type[ResponseT], override_cast_to) @@ -825,7 +825,7 @@ def __init__( version: str, base_url: str | URL, max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, @@ -1356,7 +1356,7 @@ def __init__( base_url: str | URL, _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, @@ -1818,8 +1818,8 @@ def make_request_options( extra_query: Query | None = None, extra_body: Body | None = None, idempotency_key: str | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - post_parser: PostParser | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + post_parser: PostParser | NotGiven = not_given, ) -> RequestOptions: """Create a dict of type RequestOptions without keys of NotGiven values.""" options: RequestOptions = {} diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py index 27572c6..19a598a 100644 --- a/src/cas_parser/_client.py +++ b/src/cas_parser/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Union, Mapping +from typing import Any, Mapping from typing_extensions import Self, override import httpx @@ -11,13 +11,13 @@ from . import _exceptions from ._qs import Querystring from ._types import ( - NOT_GIVEN, Omit, Timeout, NotGiven, Transport, ProxiesTypes, RequestOptions, + not_given, ) from ._utils import is_given, get_async_library from ._version import __version__ @@ -56,7 +56,7 @@ def __init__( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, @@ -132,9 +132,9 @@ def copy( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, - max_retries: int | NotGiven = NOT_GIVEN, + max_retries: int | NotGiven = not_given, default_headers: Mapping[str, str] | None = None, set_default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, @@ -226,7 +226,7 @@ def __init__( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, @@ -302,9 +302,9 @@ def copy( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, - max_retries: int | NotGiven = NOT_GIVEN, + max_retries: int | NotGiven = not_given, default_headers: Mapping[str, str] | None = None, set_default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, diff --git a/src/cas_parser/_qs.py b/src/cas_parser/_qs.py index 274320c..ada6fd3 100644 --- a/src/cas_parser/_qs.py +++ b/src/cas_parser/_qs.py @@ -4,7 +4,7 @@ from urllib.parse import parse_qs, urlencode from typing_extensions import Literal, get_args -from ._types import NOT_GIVEN, NotGiven, NotGivenOr +from ._types import NotGiven, not_given from ._utils import flatten _T = TypeVar("_T") @@ -41,8 +41,8 @@ def stringify( self, params: Params, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, ) -> str: return urlencode( self.stringify_items( @@ -56,8 +56,8 @@ def stringify_items( self, params: Params, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, ) -> list[tuple[str, str]]: opts = Options( qs=self, @@ -143,8 +143,8 @@ def __init__( self, qs: Querystring = _qs, *, - array_format: NotGivenOr[ArrayFormat] = NOT_GIVEN, - nested_format: NotGivenOr[NestedFormat] = NOT_GIVEN, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, ) -> None: self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/src/cas_parser/_types.py b/src/cas_parser/_types.py index 920e967..b45034b 100644 --- a/src/cas_parser/_types.py +++ b/src/cas_parser/_types.py @@ -117,18 +117,21 @@ class RequestOptions(TypedDict, total=False): # Sentinel class used until PEP 0661 is accepted class NotGiven: """ - A sentinel singleton class used to distinguish omitted keyword arguments - from those passed in with the value None (which may have different behavior). + For parameters with a meaningful None value, we need to distinguish between + the user explicitly passing None, and the user not passing the parameter at + all. + + User code shouldn't need to use not_given directly. For example: ```py - def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... + def create(timeout: Timeout | None | NotGiven = not_given): ... - get(timeout=1) # 1s timeout - get(timeout=None) # No timeout - get() # Default timeout behavior, which may not be statically known at the method definition. + create(timeout=1) # 1s timeout + create(timeout=None) # No timeout + create() # Default timeout behavior ``` """ @@ -140,13 +143,14 @@ def __repr__(self) -> str: return "NOT_GIVEN" -NotGivenOr = Union[_T, NotGiven] +not_given = NotGiven() +# for backwards compatibility: NOT_GIVEN = NotGiven() class Omit: - """In certain situations you need to be able to represent a case where a default value has - to be explicitly removed and `None` is not an appropriate substitute, for example: + """ + To explicitly omit something from being sent in a request, use `omit`. ```py # as the default `Content-Type` header is `application/json` that will be sent @@ -156,8 +160,8 @@ class Omit: # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' client.post(..., headers={"Content-Type": "multipart/form-data"}) - # instead you can remove the default `application/json` header by passing Omit - client.post(..., headers={"Content-Type": Omit()}) + # instead you can remove the default `application/json` header by passing omit + client.post(..., headers={"Content-Type": omit}) ``` """ @@ -165,6 +169,9 @@ def __bool__(self) -> Literal[False]: return False +omit = Omit() + + @runtime_checkable class ModelBuilderProtocol(Protocol): @classmethod diff --git a/src/cas_parser/_utils/_transform.py b/src/cas_parser/_utils/_transform.py index c19124f..5207549 100644 --- a/src/cas_parser/_utils/_transform.py +++ b/src/cas_parser/_utils/_transform.py @@ -268,7 +268,7 @@ def _transform_typeddict( annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): if not is_given(value): - # we don't need to include `NotGiven` values here as they'll + # we don't need to include omitted values here as they'll # be stripped out before the request is sent anyway continue @@ -434,7 +434,7 @@ async def _async_transform_typeddict( annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): if not is_given(value): - # we don't need to include `NotGiven` values here as they'll + # we don't need to include omitted values here as they'll # be stripped out before the request is sent anyway continue diff --git a/src/cas_parser/_utils/_utils.py b/src/cas_parser/_utils/_utils.py index f081859..50d5926 100644 --- a/src/cas_parser/_utils/_utils.py +++ b/src/cas_parser/_utils/_utils.py @@ -21,7 +21,7 @@ import sniffio -from .._types import NotGiven, FileTypes, NotGivenOr, HeadersLike +from .._types import Omit, NotGiven, FileTypes, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -63,7 +63,7 @@ def _extract_items( try: key = path[index] except IndexError: - if isinstance(obj, NotGiven): + if not is_given(obj): # no value was provided - we can safely ignore return [] @@ -126,8 +126,8 @@ def _extract_items( return [] -def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]: - return not isinstance(obj, NotGiven) +def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: + return not isinstance(obj, NotGiven) and not isinstance(obj, Omit) # Type safe methods for narrowing types with TypeVars. diff --git a/src/cas_parser/resources/cas_generator.py b/src/cas_parser/resources/cas_generator.py index 511b893..26ff32a 100644 --- a/src/cas_parser/resources/cas_generator.py +++ b/src/cas_parser/resources/cas_generator.py @@ -7,7 +7,7 @@ import httpx from ..types import cas_generator_generate_cas_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -50,14 +50,14 @@ def generate_cas( from_date: str, password: str, to_date: str, - cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] | NotGiven = NOT_GIVEN, - pan_no: str | NotGiven = NOT_GIVEN, + cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] | Omit = omit, + pan_no: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> CasGeneratorGenerateCasResponse: """ This endpoint generates CAS (Consolidated Account Statement) documents by @@ -133,14 +133,14 @@ async def generate_cas( from_date: str, password: str, to_date: str, - cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] | NotGiven = NOT_GIVEN, - pan_no: str | NotGiven = NOT_GIVEN, + cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] | Omit = omit, + pan_no: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> CasGeneratorGenerateCasResponse: """ This endpoint generates CAS (Consolidated Account Statement) documents by diff --git a/src/cas_parser/resources/cas_parser.py b/src/cas_parser/resources/cas_parser.py index a64b7dd..e82b0e9 100644 --- a/src/cas_parser/resources/cas_parser.py +++ b/src/cas_parser/resources/cas_parser.py @@ -12,7 +12,7 @@ cas_parser_smart_parse_params, cas_parser_cams_kfintech_params, ) -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -51,15 +51,15 @@ def with_streaming_response(self) -> CasParserResourceWithStreamingResponse: def cams_kfintech( self, *, - password: str | NotGiven = NOT_GIVEN, - pdf_file: str | NotGiven = NOT_GIVEN, - pdf_url: str | NotGiven = NOT_GIVEN, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UnifiedResponse: """ This endpoint specifically parses CAMS/KFintech CAS (Consolidated Account @@ -107,15 +107,15 @@ def cams_kfintech( def cdsl( self, *, - password: str | NotGiven = NOT_GIVEN, - pdf_file: str | NotGiven = NOT_GIVEN, - pdf_url: str | NotGiven = NOT_GIVEN, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UnifiedResponse: """ This endpoint specifically parses CDSL CAS (Consolidated Account Statement) PDF @@ -163,15 +163,15 @@ def cdsl( def nsdl( self, *, - password: str | NotGiven = NOT_GIVEN, - pdf_file: str | NotGiven = NOT_GIVEN, - pdf_url: str | NotGiven = NOT_GIVEN, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UnifiedResponse: """ This endpoint specifically parses NSDL CAS (Consolidated Account Statement) PDF @@ -219,15 +219,15 @@ def nsdl( def smart_parse( self, *, - password: str | NotGiven = NOT_GIVEN, - pdf_file: str | NotGiven = NOT_GIVEN, - pdf_url: str | NotGiven = NOT_GIVEN, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UnifiedResponse: """ This endpoint parses CAS (Consolidated Account Statement) PDF files from NSDL, @@ -297,15 +297,15 @@ def with_streaming_response(self) -> AsyncCasParserResourceWithStreamingResponse async def cams_kfintech( self, *, - password: str | NotGiven = NOT_GIVEN, - pdf_file: str | NotGiven = NOT_GIVEN, - pdf_url: str | NotGiven = NOT_GIVEN, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UnifiedResponse: """ This endpoint specifically parses CAMS/KFintech CAS (Consolidated Account @@ -353,15 +353,15 @@ async def cams_kfintech( async def cdsl( self, *, - password: str | NotGiven = NOT_GIVEN, - pdf_file: str | NotGiven = NOT_GIVEN, - pdf_url: str | NotGiven = NOT_GIVEN, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UnifiedResponse: """ This endpoint specifically parses CDSL CAS (Consolidated Account Statement) PDF @@ -409,15 +409,15 @@ async def cdsl( async def nsdl( self, *, - password: str | NotGiven = NOT_GIVEN, - pdf_file: str | NotGiven = NOT_GIVEN, - pdf_url: str | NotGiven = NOT_GIVEN, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UnifiedResponse: """ This endpoint specifically parses NSDL CAS (Consolidated Account Statement) PDF @@ -465,15 +465,15 @@ async def nsdl( async def smart_parse( self, *, - password: str | NotGiven = NOT_GIVEN, - pdf_file: str | NotGiven = NOT_GIVEN, - pdf_url: str | NotGiven = NOT_GIVEN, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UnifiedResponse: """ This endpoint parses CAS (Consolidated Account Statement) PDF files from NSDL, diff --git a/tests/test_transform.py b/tests/test_transform.py index ce97c84..451ddf6 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -8,7 +8,7 @@ import pytest -from cas_parser._types import NOT_GIVEN, Base64FileInput +from cas_parser._types import Base64FileInput, omit, not_given from cas_parser._utils import ( PropertyInfo, transform as _transform, @@ -450,4 +450,11 @@ async def test_transform_skipping(use_async: bool) -> None: @pytest.mark.asyncio async def test_strips_notgiven(use_async: bool) -> None: assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} - assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {} + assert await transform({"foo_bar": not_given}, Foo1, use_async) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_strips_omit(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": omit}, Foo1, use_async) == {} From 8e41c11b917d41f16bc8b336344bc7bbae9a7cd2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 02:34:58 +0000 Subject: [PATCH 027/116] chore: do not install brew dependencies in ./scripts/bootstrap by default --- scripts/bootstrap | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index e84fe62..b430fee 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,10 +4,18 @@ set -e cd "$(dirname "$0")/.." -if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { - echo "==> Installing Homebrew dependencies…" - brew bundle + echo -n "==> Install Homebrew dependencies? (y/N): " + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + brew bundle + ;; + *) + ;; + esac + echo } fi From e8c26445d741159768fbc6dd53367d5724a90d38 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 00:18:21 +0000 Subject: [PATCH 028/116] feat(api): api update --- .stats.yml | 4 +- src/cas_parser/types/unified_response.py | 97 ++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 92721c7..06e7614 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-b7fdba3d3f97c7debc22c7ca30b828bce81bcd64648df8c94029b27a3321ebb9.yml -openapi_spec_hash: 03f1315f1d32ada42445ca920f047dff +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-9eaed98ce5934f11e901cef376a28257d2c196bd3dba7c690babc6741a730ded.yml +openapi_spec_hash: b76e4e830c4d03ba4cf9429bb9fb9c8a config_hash: cb5d75abef6264b5d86448caf7295afa diff --git a/src/cas_parser/types/unified_response.py b/src/cas_parser/types/unified_response.py index 7dc5439..4c17c58 100644 --- a/src/cas_parser/types/unified_response.py +++ b/src/cas_parser/types/unified_response.py @@ -18,6 +18,7 @@ "DematAccountHoldingsDematMutualFund", "DematAccountHoldingsEquity", "DematAccountHoldingsGovernmentSecurity", + "DematAccountLinkedHolder", "Insurance", "InsuranceLifeInsurancePolicy", "Investor", @@ -25,15 +26,21 @@ "MetaStatementPeriod", "MutualFund", "MutualFundAdditionalInfo", + "MutualFundLinkedHolder", "MutualFundScheme", "MutualFundSchemeAdditionalInfo", "MutualFundSchemeGain", "MutualFundSchemeTransaction", + "Np", + "NpFund", + "NpFundAdditionalInfo", + "NpLinkedHolder", "Summary", "SummaryAccounts", "SummaryAccountsDemat", "SummaryAccountsInsurance", "SummaryAccountsMutualFunds", + "SummaryAccountsNps", ] @@ -160,6 +167,14 @@ class DematAccountHoldings(BaseModel): government_securities: Optional[List[DematAccountHoldingsGovernmentSecurity]] = None +class DematAccountLinkedHolder(BaseModel): + name: Optional[str] = None + """Name of the account holder""" + + pan: Optional[str] = None + """PAN of the account holder""" + + class DematAccount(BaseModel): additional_info: Optional[DematAccountAdditionalInfo] = None """Additional information specific to the demat account type""" @@ -181,6 +196,9 @@ class DematAccount(BaseModel): holdings: Optional[DematAccountHoldings] = None + linked_holders: Optional[List[DematAccountLinkedHolder]] = None + """List of account holders linked to this demat account""" + value: Optional[float] = None """Total value of the demat account""" @@ -270,6 +288,14 @@ class MutualFundAdditionalInfo(BaseModel): """PAN KYC status""" +class MutualFundLinkedHolder(BaseModel): + name: Optional[str] = None + """Name of the account holder""" + + pan: Optional[str] = None + """PAN of the account holder""" + + class MutualFundSchemeAdditionalInfo(BaseModel): advisor: Optional[str] = None """Financial advisor name (CAMS/KFintech)""" @@ -370,6 +396,9 @@ class MutualFund(BaseModel): folio_number: Optional[str] = None """Folio number""" + linked_holders: Optional[List[MutualFundLinkedHolder]] = None + """List of account holders linked to this mutual fund folio""" + registrar: Optional[str] = None """Registrar and Transfer Agent name""" @@ -379,6 +408,61 @@ class MutualFund(BaseModel): """Total value of the folio""" +class NpFundAdditionalInfo(BaseModel): + manager: Optional[str] = None + """Fund manager name""" + + tier: Optional[Literal[1, 2]] = None + """NPS tier (Tier I or Tier II)""" + + +class NpFund(BaseModel): + additional_info: Optional[NpFundAdditionalInfo] = None + """Additional information specific to the NPS fund""" + + cost: Optional[float] = None + """Cost of investment""" + + name: Optional[str] = None + """Name of the NPS fund""" + + nav: Optional[float] = None + """Net Asset Value per unit""" + + units: Optional[float] = None + """Number of units held""" + + value: Optional[float] = None + """Current market value of the holding""" + + +class NpLinkedHolder(BaseModel): + name: Optional[str] = None + """Name of the account holder""" + + pan: Optional[str] = None + """PAN of the account holder""" + + +class Np(BaseModel): + additional_info: Optional[object] = None + """Additional information specific to the NPS account""" + + cra: Optional[str] = None + """Central Record Keeping Agency name""" + + funds: Optional[List[NpFund]] = None + + linked_holders: Optional[List[NpLinkedHolder]] = None + """List of account holders linked to this NPS account""" + + pran: Optional[str] = None + """Permanent Retirement Account Number (PRAN)""" + + value: Optional[float] = None + """Total value of the NPS account""" + + class SummaryAccountsDemat(BaseModel): count: Optional[int] = None """Number of demat accounts""" @@ -403,6 +487,14 @@ class SummaryAccountsMutualFunds(BaseModel): """Total value of mutual funds""" +class SummaryAccountsNps(BaseModel): + count: Optional[int] = None + """Number of NPS accounts""" + + total_value: Optional[float] = None + """Total value of NPS accounts""" + + class SummaryAccounts(BaseModel): demat: Optional[SummaryAccountsDemat] = None @@ -410,6 +502,8 @@ class SummaryAccounts(BaseModel): mutual_funds: Optional[SummaryAccountsMutualFunds] = None + nps: Optional[SummaryAccountsNps] = None + class Summary(BaseModel): accounts: Optional[SummaryAccounts] = None @@ -429,4 +523,7 @@ class UnifiedResponse(BaseModel): mutual_funds: Optional[List[MutualFund]] = None + nps: Optional[List[Np]] = None + """List of NPS accounts""" + summary: Optional[Summary] = None From 44c5fb43c7e7af578d524ed5c31f8d6ca9cbeb33 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 02:11:43 +0000 Subject: [PATCH 029/116] chore(internal): detect missing future annotations with ruff --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 33ccf0d..dea9b6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,6 +224,8 @@ select = [ "B", # remove unused imports "F401", + # check for missing future annotations + "FA102", # bare except statements "E722", # unused arguments @@ -246,6 +248,8 @@ unfixable = [ "T203", ] +extend-safe-fixes = ["FA102"] + [tool.ruff.lint.flake8-tidy-imports.banned-api] "functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" From f558d573c3003365e7f7a43e7f4c7fa7035153ca Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 02:06:50 +0000 Subject: [PATCH 030/116] chore: bump `httpx-aiohttp` version to 0.1.9 --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- requirements.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dea9b6b..39ff931 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Homepage = "https://github.com/CASParser/cas-parser-python" Repository = "https://github.com/CASParser/cas-parser-python" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index cd93fa9..e3df62e 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -56,7 +56,7 @@ httpx==0.28.1 # via cas-parser-python # via httpx-aiohttp # via respx -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via cas-parser-python idna==3.4 # via anyio diff --git a/requirements.lock b/requirements.lock index c02016b..dde95d9 100644 --- a/requirements.lock +++ b/requirements.lock @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via cas-parser-python # via httpx-aiohttp -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via cas-parser-python idna==3.4 # via anyio From 723e06a4fbe920b3639f6676aa171c0bf8f6197e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 02:11:49 +0000 Subject: [PATCH 031/116] fix(client): close streams without requiring full consumption --- src/cas_parser/_streaming.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/cas_parser/_streaming.py b/src/cas_parser/_streaming.py index 9c9eb3e..48dca86 100644 --- a/src/cas_parser/_streaming.py +++ b/src/cas_parser/_streaming.py @@ -57,9 +57,8 @@ def __stream__(self) -> Iterator[_T]: for sse in iterator: yield process_data(data=sse.json(), cast_to=cast_to, response=response) - # Ensure the entire stream is consumed - for _sse in iterator: - ... + # As we might not fully consume the response stream, we need to close it explicitly + response.close() def __enter__(self) -> Self: return self @@ -121,9 +120,8 @@ async def __stream__(self) -> AsyncIterator[_T]: async for sse in iterator: yield process_data(data=sse.json(), cast_to=cast_to, response=response) - # Ensure the entire stream is consumed - async for _sse in iterator: - ... + # As we might not fully consume the response stream, we need to close it explicitly + await response.aclose() async def __aenter__(self) -> Self: return self From eb1a9ce1a2f0e1682af4eabae7e2c4fe83522289 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 02:28:04 +0000 Subject: [PATCH 032/116] chore(internal/tests): avoid race condition with implicit client cleanup --- tests/test_client.py | 362 +++++++++++++++++++++++-------------------- 1 file changed, 198 insertions(+), 164 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index e5b787e..47523fb 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -59,51 +59,49 @@ def _get_open_connections(client: CasParser | AsyncCasParser) -> int: class TestCasParser: - client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - def test_raw_response(self, respx_mock: MockRouter) -> None: + def test_raw_response(self, respx_mock: MockRouter, client: CasParser) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.post("/foo", cast_to=httpx.Response) + response = client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + def test_raw_response_for_binary(self, respx_mock: MockRouter, client: CasParser) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) - response = self.client.post("/foo", cast_to=httpx.Response) + response = client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, client: CasParser) -> None: + copied = client.copy() + assert id(copied) != id(client) - copied = self.client.copy(api_key="another My API Key") + copied = client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, client: CasParser) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + assert client.max_retries == 2 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 assert copied.max_retries == 7 # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(client.timeout, httpx.Timeout) + copied = client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: client = CasParser( @@ -138,6 +136,7 @@ def test_copy_default_headers(self) -> None: match="`default_headers` and `set_default_headers` arguments are mutually exclusive", ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + client.close() def test_copy_default_query(self) -> None: client = CasParser( @@ -175,13 +174,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + client.close() + + def test_copy_signature(self, client: CasParser) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] + client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -192,12 +193,12 @@ def test_copy_signature(self) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self) -> None: + def test_copy_build_request(self, client: CasParser) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -254,14 +255,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + def test_request_timeout(self, client: CasParser) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT - request = self.client._build_request( - FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) - ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(100.0) @@ -274,6 +273,8 @@ def test_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(0) + client.close() + def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: @@ -285,6 +286,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(None) + client.close() + # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: client = CasParser( @@ -295,6 +298,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT + client.close() + # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = CasParser( @@ -305,6 +310,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT # our default + client.close() + async def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): async with httpx.AsyncClient() as http_client: @@ -316,14 +323,14 @@ async def test_invalid_http_client(self) -> None: ) def test_default_headers_option(self) -> None: - client = CasParser( + test_client = CasParser( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = CasParser( + test_client2 = CasParser( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -332,10 +339,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + test_client.close() + test_client2.close() + def test_validate_headers(self) -> None: client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -364,8 +374,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + client.close() + + def test_request_extra_json(self, client: CasParser) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -376,7 +388,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -387,7 +399,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -398,8 +410,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: CasParser) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -409,7 +421,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -420,8 +432,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: CasParser) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -434,7 +446,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -448,7 +460,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -491,7 +503,7 @@ def test_multipart_repeating_array(self, client: CasParser) -> None: ] @pytest.mark.respx(base_url=base_url) - def test_basic_union_response(self, respx_mock: MockRouter) -> None: + def test_basic_union_response(self, respx_mock: MockRouter, client: CasParser) -> None: class Model1(BaseModel): name: str @@ -500,12 +512,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + def test_union_response_different_types(self, respx_mock: MockRouter, client: CasParser) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -516,18 +528,18 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model1) assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: CasParser) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -543,7 +555,7 @@ class Model(BaseModel): ) ) - response = self.client.get("/foo", cast_to=Model) + response = client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 @@ -555,6 +567,8 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" + client.close() + def test_base_url_env(self) -> None: with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): client = CasParser(api_key=api_key, _strict_response_validation=True) @@ -582,6 +596,7 @@ def test_base_url_trailing_slash(self, client: CasParser) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -605,6 +620,7 @@ def test_base_url_no_trailing_slash(self, client: CasParser) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -628,35 +644,36 @@ def test_absolute_request_url(self, client: CasParser) -> None: ), ) assert request.url == "https://myapi.com/foo" + client.close() def test_copied_client_does_not_close_http(self) -> None: - client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied - assert not client.is_closed() + assert not test_client.is_closed() def test_client_context_manager(self) -> None: - client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) - with client as c2: - assert c2 is client + test_client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + with test_client as c2: + assert c2 is test_client assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + def test_client_response_validation_error(self, respx_mock: MockRouter, client: CasParser) -> None: class Model(BaseModel): foo: str respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) with pytest.raises(APIResponseValidationError) as exc: - self.client.get("/foo", cast_to=Model) + client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -676,11 +693,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=False) - response = client.get("/foo", cast_to=Model) + response = non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] + strict_client.close() + non_strict_client.close() + @pytest.mark.parametrize( "remaining_retries,retry_after,timeout", [ @@ -703,9 +723,9 @@ class Model(BaseModel): ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = CasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, client: CasParser + ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) @@ -719,7 +739,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien with pytest.raises(APITimeoutError): client.cas_parser.with_streaming_response.smart_parse().__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -728,7 +748,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client with pytest.raises(APIStatusError): client.cas_parser.with_streaming_response.smart_parse().__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -830,83 +850,77 @@ def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - def test_follow_redirects(self, respx_mock: MockRouter) -> None: + def test_follow_redirects(self, respx_mock: MockRouter, client: CasParser) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) assert response.status_code == 200 assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: CasParser) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) with pytest.raises(APIStatusError) as exc_info: - self.client.post( - "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response - ) + client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response) assert exc_info.value.response.status_code == 302 assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" class TestAsyncCasParser: - client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response(self, respx_mock: MockRouter) -> None: + async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.post("/foo", cast_to=httpx.Response) + response = await async_client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) - response = await self.client.post("/foo", cast_to=httpx.Response) + response = await async_client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, async_client: AsyncCasParser) -> None: + copied = async_client.copy() + assert id(copied) != id(async_client) - copied = self.client.copy(api_key="another My API Key") + copied = async_client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert async_client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, async_client: AsyncCasParser) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = async_client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + assert async_client.max_retries == 2 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 assert copied.max_retries == 7 # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(async_client.timeout, httpx.Timeout) + copied = async_client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(async_client.timeout, httpx.Timeout) - def test_copy_default_headers(self) -> None: + async def test_copy_default_headers(self) -> None: client = AsyncCasParser( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) @@ -939,8 +953,9 @@ def test_copy_default_headers(self) -> None: match="`default_headers` and `set_default_headers` arguments are mutually exclusive", ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + await client.close() - def test_copy_default_query(self) -> None: + async def test_copy_default_query(self) -> None: client = AsyncCasParser( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) @@ -976,13 +991,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + await client.close() + + def test_copy_signature(self, async_client: AsyncCasParser) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] + async_client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(async_client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -993,12 +1010,12 @@ def test_copy_signature(self) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self) -> None: + def test_copy_build_request(self, async_client: AsyncCasParser) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = async_client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -1055,12 +1072,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - async def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + async def test_request_timeout(self, async_client: AsyncCasParser) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT - request = self.client._build_request( + request = async_client._build_request( FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) ) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -1075,6 +1092,8 @@ async def test_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(0) + await client.close() + async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: @@ -1086,6 +1105,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(None) + await client.close() + # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: client = AsyncCasParser( @@ -1096,6 +1117,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT + await client.close() + # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = AsyncCasParser( @@ -1106,6 +1129,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT # our default + await client.close() + def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): with httpx.Client() as http_client: @@ -1116,15 +1141,15 @@ def test_invalid_http_client(self) -> None: http_client=cast(Any, http_client), ) - def test_default_headers_option(self) -> None: - client = AsyncCasParser( + async def test_default_headers_option(self) -> None: + test_client = AsyncCasParser( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = AsyncCasParser( + test_client2 = AsyncCasParser( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1133,10 +1158,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + await test_client.close() + await test_client2.close() + def test_validate_headers(self) -> None: client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1147,7 +1175,7 @@ def test_validate_headers(self) -> None: client2 = AsyncCasParser(base_url=base_url, api_key=None, _strict_response_validation=True) _ = client2 - def test_default_query_option(self) -> None: + async def test_default_query_option(self) -> None: client = AsyncCasParser( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) @@ -1165,8 +1193,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + await client.close() + + def test_request_extra_json(self, client: CasParser) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1177,7 +1207,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1188,7 +1218,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1199,8 +1229,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: CasParser) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1210,7 +1240,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1221,8 +1251,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: CasParser) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1235,7 +1265,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1249,7 +1279,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1292,7 +1322,7 @@ def test_multipart_repeating_array(self, async_client: AsyncCasParser) -> None: ] @pytest.mark.respx(base_url=base_url) - async def test_basic_union_response(self, respx_mock: MockRouter) -> None: + async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None: class Model1(BaseModel): name: str @@ -1301,12 +1331,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - async def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -1317,18 +1347,20 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model1) assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + async def test_non_application_json_content_type_for_json_data( + self, respx_mock: MockRouter, async_client: AsyncCasParser + ) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -1344,11 +1376,11 @@ class Model(BaseModel): ) ) - response = await self.client.get("/foo", cast_to=Model) + response = await async_client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 - def test_base_url_setter(self) -> None: + async def test_base_url_setter(self) -> None: client = AsyncCasParser( base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) @@ -1358,7 +1390,9 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" - def test_base_url_env(self) -> None: + await client.close() + + async def test_base_url_env(self) -> None: with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): client = AsyncCasParser(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @@ -1378,7 +1412,7 @@ def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_trailing_slash(self, client: AsyncCasParser) -> None: + async def test_base_url_trailing_slash(self, client: AsyncCasParser) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1387,6 +1421,7 @@ def test_base_url_trailing_slash(self, client: AsyncCasParser) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1403,7 +1438,7 @@ def test_base_url_trailing_slash(self, client: AsyncCasParser) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_no_trailing_slash(self, client: AsyncCasParser) -> None: + async def test_base_url_no_trailing_slash(self, client: AsyncCasParser) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1412,6 +1447,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncCasParser) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1428,7 +1464,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncCasParser) -> None: ], ids=["standard", "custom http client"], ) - def test_absolute_request_url(self, client: AsyncCasParser) -> None: + async def test_absolute_request_url(self, client: AsyncCasParser) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1437,37 +1473,37 @@ def test_absolute_request_url(self, client: AsyncCasParser) -> None: ), ) assert request.url == "https://myapi.com/foo" + await client.close() async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied await asyncio.sleep(0.2) - assert not client.is_closed() + assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) - async with client as c2: - assert c2 is client + test_client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) + async with test_client as c2: + assert c2 is test_client assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None: class Model(BaseModel): foo: str respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) with pytest.raises(APIResponseValidationError) as exc: - await self.client.get("/foo", cast_to=Model) + await async_client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -1478,7 +1514,6 @@ async def test_client_max_retries_validation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: class Model(BaseModel): name: str @@ -1490,11 +1525,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=False) - response = await client.get("/foo", cast_to=Model) + response = await non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] + await strict_client.close() + await non_strict_client.close() + @pytest.mark.parametrize( "remaining_retries,retry_after,timeout", [ @@ -1517,13 +1555,12 @@ class Model(BaseModel): ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - @pytest.mark.asyncio - async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = AsyncCasParser(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + async def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncCasParser + ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) - calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -1536,7 +1573,7 @@ async def test_retrying_timeout_errors_doesnt_leak( with pytest.raises(APITimeoutError): await async_client.cas_parser.with_streaming_response.smart_parse().__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -1547,12 +1584,11 @@ async def test_retrying_status_errors_doesnt_leak( with pytest.raises(APIStatusError): await async_client.cas_parser.with_streaming_response.smart_parse().__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio @pytest.mark.parametrize("failure_mode", ["status", "exception"]) async def test_retries_taken( self, @@ -1584,7 +1620,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_omit_retry_count_header( self, async_client: AsyncCasParser, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1610,7 +1645,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_overwrite_retry_count_header( self, async_client: AsyncCasParser, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1660,26 +1694,26 @@ async def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects(self, respx_mock: MockRouter) -> None: + async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) assert response.status_code == 200 assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) with pytest.raises(APIStatusError) as exc_info: - await self.client.post( + await async_client.post( "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response ) From b2fc6345cc4153f13c3fe1fd69e59d1944c8f63b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 03:36:25 +0000 Subject: [PATCH 033/116] chore(internal): grammar fix (it's -> its) --- src/cas_parser/_utils/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cas_parser/_utils/_utils.py b/src/cas_parser/_utils/_utils.py index 50d5926..eec7f4a 100644 --- a/src/cas_parser/_utils/_utils.py +++ b/src/cas_parser/_utils/_utils.py @@ -133,7 +133,7 @@ def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: # Type safe methods for narrowing types with TypeVars. # The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], # however this cause Pyright to rightfully report errors. As we know we don't -# care about the contained types we can safely use `object` in it's place. +# care about the contained types we can safely use `object` in its place. # # There are two separate functions defined, `is_*` and `is_*_t` for different use cases. # `is_*` is for when you're dealing with an unknown input From 3cc7e7d7c6d7864435c8236ddd6d76815ab940c3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 03:40:57 +0000 Subject: [PATCH 034/116] chore(internal): codegen related update --- README.md | 4 ++-- pyproject.toml | 5 ++--- src/cas_parser/_models.py | 11 ++++++++--- src/cas_parser/_utils/_sync.py | 34 +++------------------------------- tests/test_models.py | 8 ++++---- 5 files changed, 19 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index c13a2c4..798c044 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://img.shields.io/pypi/v/cas-parser-python.svg?label=pypi%20(stable))](https://pypi.org/project/cas-parser-python/) -The Cas Parser Python library provides convenient access to the Cas Parser REST API from any Python 3.8+ +The Cas Parser Python library provides convenient access to the Cas Parser REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -380,7 +380,7 @@ print(cas_parser.__version__) ## Requirements -Python 3.8 or higher. +Python 3.9 or higher. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index 39ff931..a87c6f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,10 @@ dependencies = [ "distro>=1.7.0, <2", "sniffio", ] -requires-python = ">= 3.8" +requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -141,7 +140,7 @@ filterwarnings = [ # there are a couple of flags that are still disabled by # default in strict mode as they are experimental and niche. typeCheckingMode = "strict" -pythonVersion = "3.8" +pythonVersion = "3.9" exclude = [ "_dev", diff --git a/src/cas_parser/_models.py b/src/cas_parser/_models.py index 6a3cd1d..fcec2cf 100644 --- a/src/cas_parser/_models.py +++ b/src/cas_parser/_models.py @@ -2,6 +2,7 @@ import os import inspect +import weakref from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast from datetime import date, datetime from typing_extensions import ( @@ -573,6 +574,9 @@ class CachedDiscriminatorType(Protocol): __discriminator__: DiscriminatorDetails +DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary() + + class DiscriminatorDetails: field_name: str """The name of the discriminator field in the variant class, e.g. @@ -615,8 +619,9 @@ def __init__( def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: - if isinstance(union, CachedDiscriminatorType): - return union.__discriminator__ + cached = DISCRIMINATOR_CACHE.get(union) + if cached is not None: + return cached discriminator_field_name: str | None = None @@ -669,7 +674,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, discriminator_field=discriminator_field_name, discriminator_alias=discriminator_alias, ) - cast(CachedDiscriminatorType, union).__discriminator__ = details + DISCRIMINATOR_CACHE.setdefault(union, details) return details diff --git a/src/cas_parser/_utils/_sync.py b/src/cas_parser/_utils/_sync.py index ad7ec71..f6027c1 100644 --- a/src/cas_parser/_utils/_sync.py +++ b/src/cas_parser/_utils/_sync.py @@ -1,10 +1,8 @@ from __future__ import annotations -import sys import asyncio import functools -import contextvars -from typing import Any, TypeVar, Callable, Awaitable +from typing import TypeVar, Callable, Awaitable from typing_extensions import ParamSpec import anyio @@ -15,34 +13,11 @@ T_ParamSpec = ParamSpec("T_ParamSpec") -if sys.version_info >= (3, 9): - _asyncio_to_thread = asyncio.to_thread -else: - # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread - # for Python 3.8 support - async def _asyncio_to_thread( - func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs - ) -> Any: - """Asynchronously run function *func* in a separate thread. - - Any *args and **kwargs supplied for this function are directly passed - to *func*. Also, the current :class:`contextvars.Context` is propagated, - allowing context variables from the main thread to be accessed in the - separate thread. - - Returns a coroutine that can be awaited to get the eventual result of *func*. - """ - loop = asyncio.events.get_running_loop() - ctx = contextvars.copy_context() - func_call = functools.partial(ctx.run, func, *args, **kwargs) - return await loop.run_in_executor(None, func_call) - - async def to_thread( func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs ) -> T_Retval: if sniffio.current_async_library() == "asyncio": - return await _asyncio_to_thread(func, *args, **kwargs) + return await asyncio.to_thread(func, *args, **kwargs) return await anyio.to_thread.run_sync( functools.partial(func, *args, **kwargs), @@ -53,10 +28,7 @@ async def to_thread( def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ Take a blocking function and create an async one that receives the same - positional and keyword arguments. For python version 3.9 and above, it uses - asyncio.to_thread to run the function in a separate thread. For python version - 3.8, it uses locally defined copy of the asyncio.to_thread function which was - introduced in python 3.9. + positional and keyword arguments. Usage: diff --git a/tests/test_models.py b/tests/test_models.py index ffd0d05..82ce6d4 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -9,7 +9,7 @@ from cas_parser._utils import PropertyInfo from cas_parser._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from cas_parser._models import BaseModel, construct_type +from cas_parser._models import DISCRIMINATOR_CACHE, BaseModel, construct_type class BasicModel(BaseModel): @@ -809,7 +809,7 @@ class B(BaseModel): UnionType = cast(Any, Union[A, B]) - assert not hasattr(UnionType, "__discriminator__") + assert not DISCRIMINATOR_CACHE.get(UnionType) m = construct_type( value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) @@ -818,7 +818,7 @@ class B(BaseModel): assert m.type == "b" assert m.data == "foo" # type: ignore[comparison-overlap] - discriminator = UnionType.__discriminator__ + discriminator = DISCRIMINATOR_CACHE.get(UnionType) assert discriminator is not None m = construct_type( @@ -830,7 +830,7 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache - assert UnionType.__discriminator__ is discriminator + assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator @pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") From 67748f6d52e3d3ac2a386280a1dc4f4dd1ac03fc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 03:33:46 +0000 Subject: [PATCH 035/116] chore(internal): codegen related update --- src/cas_parser/_models.py | 41 +++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/cas_parser/_models.py b/src/cas_parser/_models.py index fcec2cf..ca9500b 100644 --- a/src/cas_parser/_models.py +++ b/src/cas_parser/_models.py @@ -257,15 +257,16 @@ def model_dump( mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, - serialize_as_any: bool = False, fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -273,16 +274,24 @@ def model_dump( Args: mode: The mode in which `to_python` should run. - If mode is 'json', the dictionary will only contain JSON serializable types. - If mode is 'python', the dictionary may contain any Python objects. - include: A list of fields to include in the output. - exclude: A list of fields to exclude from the output. + If mode is 'json', the output will only contain JSON serializable types. + If mode is 'python', the output may contain non-JSON-serializable Python objects. + include: A set of fields to include in the output. + exclude: A set of fields to exclude from the output. + context: Additional context to pass to the serializer. by_alias: Whether to use the field's alias in the dictionary key if defined. - exclude_unset: Whether to exclude fields that are unset or None from the output. - exclude_defaults: Whether to exclude fields that are set to their default value from the output. - exclude_none: Whether to exclude fields that have a value of `None` from the output. - round_trip: Whether to enable serialization and deserialization round-trip support. - warnings: Whether to log warnings when invalid fields are encountered. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. Returns: A dictionary representation of the model. @@ -299,6 +308,8 @@ def model_dump( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, @@ -315,15 +326,17 @@ def model_dump_json( self, *, indent: int | None = None, + ensure_ascii: bool = False, include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: @@ -355,6 +368,10 @@ def model_dump_json( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if ensure_ascii != False: + raise ValueError("ensure_ascii is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, From 38fcd595342509bb287cd0d801d576f320d7b9d5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 23:17:33 +0000 Subject: [PATCH 036/116] feat(api): api update --- .stats.yml | 4 +- LICENSE | 2 +- README.md | 3 +- pyproject.toml | 17 +- requirements-dev.lock | 112 +++-- requirements.lock | 39 +- scripts/lint | 9 +- src/cas_parser/_base_client.py | 10 +- src/cas_parser/_client.py | 133 +++-- src/cas_parser/_streaming.py | 22 +- src/cas_parser/_types.py | 5 +- src/cas_parser/types/unified_response.py | 597 ++++++++++++++++++++++- 12 files changed, 808 insertions(+), 145 deletions(-) diff --git a/.stats.yml b/.stats.yml index 06e7614..48b33b3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 5 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-9eaed98ce5934f11e901cef376a28257d2c196bd3dba7c690babc6741a730ded.yml -openapi_spec_hash: b76e4e830c4d03ba4cf9429bb9fb9c8a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-38618cc5c938e87eeacf4893d6a6ba4e6ef7da390e6283dc7b50b484a7b97165.yml +openapi_spec_hash: b9e439ecee904ded01aa34efdee88856 config_hash: cb5d75abef6264b5d86448caf7295afa diff --git a/LICENSE b/LICENSE index f1756ce..6bbb512 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Cas Parser + Copyright 2026 Cas Parser Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 798c044..bfab47b 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ pip install cas-parser-python[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from cas_parser import DefaultAioHttpClient from cas_parser import AsyncCasParser @@ -92,7 +93,7 @@ from cas_parser import AsyncCasParser async def main() -> None: async with AsyncCasParser( - api_key="My API Key", + api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: unified_response = await client.cas_parser.smart_parse( diff --git a/pyproject.toml b/pyproject.toml index a87c6f9..f286b5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,14 +7,16 @@ license = "Apache-2.0" authors = [ { name = "Cas Parser", email = "sameer@casparser.in" }, ] + dependencies = [ - "httpx>=0.23.0, <1", - "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", - "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", - "sniffio", + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", ] + requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", @@ -24,6 +26,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", @@ -45,7 +48,7 @@ managed = true # version pins are in requirements-dev.lock dev-dependencies = [ "pyright==1.1.399", - "mypy", + "mypy==1.17", "respx", "pytest", "pytest-asyncio", diff --git a/requirements-dev.lock b/requirements-dev.lock index e3df62e..1a3f9c1 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,40 +12,45 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via cas-parser-python # via httpx-aiohttp -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via cas-parser-python # via httpx -argcomplete==3.1.2 +argcomplete==3.6.3 # via nox async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 + # via nox +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +certifi==2025.11.12 # via httpcore # via httpx -colorlog==6.7.0 +colorlog==6.10.1 + # via nox +dependency-groups==1.3.1 # via nox -dirty-equals==0.6.0 -distlib==0.3.7 +dirty-equals==0.11 +distlib==0.4.0 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via cas-parser-python -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio # via pytest -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist -filelock==3.12.4 +filelock==3.19.1 # via virtualenv -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -58,80 +63,87 @@ httpx==0.28.1 # via respx httpx-aiohttp==0.1.9 # via cas-parser-python -idna==3.4 +humanize==4.13.0 + # via nox +idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==7.0.0 -iniconfig==2.0.0 +importlib-metadata==8.7.0 +iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -mypy==1.14.1 -mypy-extensions==1.0.0 +mypy==1.17.0 +mypy-extensions==1.1.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright -nox==2023.4.22 -packaging==23.2 +nox==2025.11.12 +packaging==25.0 + # via dependency-groups # via nox # via pytest -platformdirs==3.11.0 +pathspec==0.12.1 + # via mypy +platformdirs==4.4.0 # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via cas-parser-python -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -pygments==2.18.0 +pygments==2.19.2 + # via pytest # via rich pyright==1.1.399 -pytest==8.3.3 +pytest==8.4.2 # via pytest-asyncio # via pytest-xdist -pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 -python-dateutil==2.8.2 +pytest-asyncio==1.2.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 # via time-machine -pytz==2023.3.post1 - # via dirty-equals respx==0.22.0 -rich==13.7.1 -ruff==0.9.4 -setuptools==68.2.2 - # via nodeenv -six==1.16.0 +rich==14.2.0 +ruff==0.14.7 +six==1.17.0 # via python-dateutil -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via cas-parser-python -time-machine==2.9.0 -tomli==2.0.2 +time-machine==2.19.0 +tomli==2.3.0 + # via dependency-groups # via mypy + # via nox # via pytest -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio # via cas-parser-python + # via exceptiongroup # via multidict # via mypy # via pydantic # via pydantic-core # via pyright + # via pytest-asyncio # via typing-inspection -typing-inspection==0.4.1 + # via virtualenv +typing-inspection==0.4.2 # via pydantic -virtualenv==20.24.5 +virtualenv==20.35.4 # via nox -yarl==1.20.0 +yarl==1.22.0 # via aiohttp -zipp==3.17.0 +zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index dde95d9..4fdd1ca 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,28 +12,28 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via cas-parser-python # via httpx-aiohttp -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via cas-parser-python # via httpx async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 +certifi==2025.11.12 # via httpcore # via httpx -distro==1.8.0 +distro==1.9.0 # via cas-parser-python -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -45,31 +45,32 @@ httpx==0.28.1 # via httpx-aiohttp httpx-aiohttp==0.1.9 # via cas-parser-python -idna==3.4 +idna==3.11 # via anyio # via httpx # via yarl -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via cas-parser-python -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via cas-parser-python -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio # via cas-parser-python + # via exceptiongroup # via multidict # via pydantic # via pydantic-core # via typing-inspection -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic -yarl==1.20.0 +yarl==1.22.0 # via aiohttp diff --git a/scripts/lint b/scripts/lint index d325f0b..e1bf7a7 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import cas_parser' diff --git a/src/cas_parser/_base_client.py b/src/cas_parser/_base_client.py index fc89c5a..9cfe0c2 100644 --- a/src/cas_parser/_base_client.py +++ b/src/cas_parser/_base_client.py @@ -1247,9 +1247,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1767,9 +1770,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py index 19a598a..f0d7ed2 100644 --- a/src/cas_parser/_client.py +++ b/src/cas_parser/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Mapping +from typing import TYPE_CHECKING, Any, Mapping from typing_extensions import Self, override import httpx @@ -20,8 +20,8 @@ not_given, ) from ._utils import is_given, get_async_library +from ._compat import cached_property from ._version import __version__ -from .resources import cas_parser, cas_generator from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, CasParserError from ._base_client import ( @@ -30,6 +30,11 @@ AsyncAPIClient, ) +if TYPE_CHECKING: + from .resources import cas_parser, cas_generator + from .resources.cas_parser import CasParserResource, AsyncCasParserResource + from .resources.cas_generator import CasGeneratorResource, AsyncCasGeneratorResource + __all__ = [ "Timeout", "Transport", @@ -43,11 +48,6 @@ class CasParser(SyncAPIClient): - cas_parser: cas_parser.CasParserResource - cas_generator: cas_generator.CasGeneratorResource - with_raw_response: CasParserWithRawResponse - with_streaming_response: CasParserWithStreamedResponse - # client options api_key: str @@ -102,10 +102,25 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.cas_parser = cas_parser.CasParserResource(self) - self.cas_generator = cas_generator.CasGeneratorResource(self) - self.with_raw_response = CasParserWithRawResponse(self) - self.with_streaming_response = CasParserWithStreamedResponse(self) + @cached_property + def cas_parser(self) -> CasParserResource: + from .resources.cas_parser import CasParserResource + + return CasParserResource(self) + + @cached_property + def cas_generator(self) -> CasGeneratorResource: + from .resources.cas_generator import CasGeneratorResource + + return CasGeneratorResource(self) + + @cached_property + def with_raw_response(self) -> CasParserWithRawResponse: + return CasParserWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasParserWithStreamedResponse: + return CasParserWithStreamedResponse(self) @property @override @@ -213,11 +228,6 @@ def _make_status_error( class AsyncCasParser(AsyncAPIClient): - cas_parser: cas_parser.AsyncCasParserResource - cas_generator: cas_generator.AsyncCasGeneratorResource - with_raw_response: AsyncCasParserWithRawResponse - with_streaming_response: AsyncCasParserWithStreamedResponse - # client options api_key: str @@ -272,10 +282,25 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.cas_parser = cas_parser.AsyncCasParserResource(self) - self.cas_generator = cas_generator.AsyncCasGeneratorResource(self) - self.with_raw_response = AsyncCasParserWithRawResponse(self) - self.with_streaming_response = AsyncCasParserWithStreamedResponse(self) + @cached_property + def cas_parser(self) -> AsyncCasParserResource: + from .resources.cas_parser import AsyncCasParserResource + + return AsyncCasParserResource(self) + + @cached_property + def cas_generator(self) -> AsyncCasGeneratorResource: + from .resources.cas_generator import AsyncCasGeneratorResource + + return AsyncCasGeneratorResource(self) + + @cached_property + def with_raw_response(self) -> AsyncCasParserWithRawResponse: + return AsyncCasParserWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasParserWithStreamedResponse: + return AsyncCasParserWithStreamedResponse(self) @property @override @@ -383,27 +408,79 @@ def _make_status_error( class CasParserWithRawResponse: + _client: CasParser + def __init__(self, client: CasParser) -> None: - self.cas_parser = cas_parser.CasParserResourceWithRawResponse(client.cas_parser) - self.cas_generator = cas_generator.CasGeneratorResourceWithRawResponse(client.cas_generator) + self._client = client + + @cached_property + def cas_parser(self) -> cas_parser.CasParserResourceWithRawResponse: + from .resources.cas_parser import CasParserResourceWithRawResponse + + return CasParserResourceWithRawResponse(self._client.cas_parser) + + @cached_property + def cas_generator(self) -> cas_generator.CasGeneratorResourceWithRawResponse: + from .resources.cas_generator import CasGeneratorResourceWithRawResponse + + return CasGeneratorResourceWithRawResponse(self._client.cas_generator) class AsyncCasParserWithRawResponse: + _client: AsyncCasParser + def __init__(self, client: AsyncCasParser) -> None: - self.cas_parser = cas_parser.AsyncCasParserResourceWithRawResponse(client.cas_parser) - self.cas_generator = cas_generator.AsyncCasGeneratorResourceWithRawResponse(client.cas_generator) + self._client = client + + @cached_property + def cas_parser(self) -> cas_parser.AsyncCasParserResourceWithRawResponse: + from .resources.cas_parser import AsyncCasParserResourceWithRawResponse + + return AsyncCasParserResourceWithRawResponse(self._client.cas_parser) + + @cached_property + def cas_generator(self) -> cas_generator.AsyncCasGeneratorResourceWithRawResponse: + from .resources.cas_generator import AsyncCasGeneratorResourceWithRawResponse + + return AsyncCasGeneratorResourceWithRawResponse(self._client.cas_generator) class CasParserWithStreamedResponse: + _client: CasParser + def __init__(self, client: CasParser) -> None: - self.cas_parser = cas_parser.CasParserResourceWithStreamingResponse(client.cas_parser) - self.cas_generator = cas_generator.CasGeneratorResourceWithStreamingResponse(client.cas_generator) + self._client = client + + @cached_property + def cas_parser(self) -> cas_parser.CasParserResourceWithStreamingResponse: + from .resources.cas_parser import CasParserResourceWithStreamingResponse + + return CasParserResourceWithStreamingResponse(self._client.cas_parser) + + @cached_property + def cas_generator(self) -> cas_generator.CasGeneratorResourceWithStreamingResponse: + from .resources.cas_generator import CasGeneratorResourceWithStreamingResponse + + return CasGeneratorResourceWithStreamingResponse(self._client.cas_generator) class AsyncCasParserWithStreamedResponse: + _client: AsyncCasParser + def __init__(self, client: AsyncCasParser) -> None: - self.cas_parser = cas_parser.AsyncCasParserResourceWithStreamingResponse(client.cas_parser) - self.cas_generator = cas_generator.AsyncCasGeneratorResourceWithStreamingResponse(client.cas_generator) + self._client = client + + @cached_property + def cas_parser(self) -> cas_parser.AsyncCasParserResourceWithStreamingResponse: + from .resources.cas_parser import AsyncCasParserResourceWithStreamingResponse + + return AsyncCasParserResourceWithStreamingResponse(self._client.cas_parser) + + @cached_property + def cas_generator(self) -> cas_generator.AsyncCasGeneratorResourceWithStreamingResponse: + from .resources.cas_generator import AsyncCasGeneratorResourceWithStreamingResponse + + return AsyncCasGeneratorResourceWithStreamingResponse(self._client.cas_generator) Client = CasParser diff --git a/src/cas_parser/_streaming.py b/src/cas_parser/_streaming.py index 48dca86..00e2105 100644 --- a/src/cas_parser/_streaming.py +++ b/src/cas_parser/_streaming.py @@ -54,11 +54,12 @@ def __stream__(self) -> Iterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - response.close() + try: + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + response.close() def __enter__(self) -> Self: return self @@ -117,11 +118,12 @@ async def __stream__(self) -> AsyncIterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - async for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - await response.aclose() + try: + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + await response.aclose() async def __aenter__(self) -> Self: return self diff --git a/src/cas_parser/_types.py b/src/cas_parser/_types.py index b45034b..2c15258 100644 --- a/src/cas_parser/_types.py +++ b/src/cas_parser/_types.py @@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False): if TYPE_CHECKING: # This works because str.__contains__ does not accept object (either in typeshed or at runtime) # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. class SequenceNotStr(Protocol[_T_co]): @overload def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... @@ -251,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T_co]: ... - def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case diff --git a/src/cas_parser/types/unified_response.py b/src/cas_parser/types/unified_response.py index 4c17c58..2a8ab94 100644 --- a/src/cas_parser/types/unified_response.py +++ b/src/cas_parser/types/unified_response.py @@ -14,10 +14,25 @@ "DematAccountAdditionalInfo", "DematAccountHoldings", "DematAccountHoldingsAif", + "DematAccountHoldingsAifAdditionalInfo", + "DematAccountHoldingsAifTransaction", + "DematAccountHoldingsAifTransactionAdditionalInfo", "DematAccountHoldingsCorporateBond", + "DematAccountHoldingsCorporateBondAdditionalInfo", + "DematAccountHoldingsCorporateBondTransaction", + "DematAccountHoldingsCorporateBondTransactionAdditionalInfo", "DematAccountHoldingsDematMutualFund", + "DematAccountHoldingsDematMutualFundAdditionalInfo", + "DematAccountHoldingsDematMutualFundTransaction", + "DematAccountHoldingsDematMutualFundTransactionAdditionalInfo", "DematAccountHoldingsEquity", + "DematAccountHoldingsEquityAdditionalInfo", + "DematAccountHoldingsEquityTransaction", + "DematAccountHoldingsEquityTransactionAdditionalInfo", "DematAccountHoldingsGovernmentSecurity", + "DematAccountHoldingsGovernmentSecurityAdditionalInfo", + "DematAccountHoldingsGovernmentSecurityTransaction", + "DematAccountHoldingsGovernmentSecurityTransactionAdditionalInfo", "DematAccountLinkedHolder", "Insurance", "InsuranceLifeInsurancePolicy", @@ -31,6 +46,7 @@ "MutualFundSchemeAdditionalInfo", "MutualFundSchemeGain", "MutualFundSchemeTransaction", + "MutualFundSchemeTransactionAdditionalInfo", "Np", "NpFund", "NpFundAdditionalInfo", @@ -45,6 +61,8 @@ class DematAccountAdditionalInfo(BaseModel): + """Additional information specific to the demat account type""" + bo_status: Optional[str] = None """Beneficiary Owner status (CDSL)""" @@ -70,8 +88,101 @@ class DematAccountAdditionalInfo(BaseModel): """Account status (CDSL)""" +class DematAccountHoldingsAifAdditionalInfo(BaseModel): + """Additional information specific to the AIF""" + + close_units: Optional[float] = None + """Closing balance units for the statement period (beta)""" + + open_units: Optional[float] = None + """Opening balance units for the statement period (beta)""" + + +class DematAccountHoldingsAifTransactionAdditionalInfo(BaseModel): + """Additional transaction-specific fields that vary by source""" + + capital_withdrawal: Optional[float] = None + """Capital withdrawal amount (CDSL MF transactions)""" + + credit: Optional[float] = None + """Units credited (demat transactions)""" + + debit: Optional[float] = None + """Units debited (demat transactions)""" + + income_distribution: Optional[float] = None + """Income distribution amount (CDSL MF transactions)""" + + order_no: Optional[str] = None + """Order/transaction reference number (demat transactions)""" + + price: Optional[float] = None + """Price per unit (NSDL/CDSL MF transactions)""" + + stamp_duty: Optional[float] = None + """Stamp duty charged""" + + +class DematAccountHoldingsAifTransaction(BaseModel): + """ + Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) + """ + + additional_info: Optional[DematAccountHoldingsAifTransactionAdditionalInfo] = None + """Additional transaction-specific fields that vary by source""" + + amount: Optional[float] = None + """Transaction amount in currency (computed from units × price/NAV)""" + + balance: Optional[float] = None + """Balance units after transaction""" + + date: Optional[datetime.date] = None + """Transaction date (YYYY-MM-DD)""" + + description: Optional[str] = None + """Transaction description/particulars""" + + dividend_rate: Optional[float] = None + """Dividend rate (for DIVIDEND_PAYOUT transactions)""" + + nav: Optional[float] = None + """NAV/price per unit on transaction date""" + + type: Optional[ + Literal[ + "PURCHASE", + "PURCHASE_SIP", + "REDEMPTION", + "SWITCH_IN", + "SWITCH_IN_MERGER", + "SWITCH_OUT", + "SWITCH_OUT_MERGER", + "DIVIDEND_PAYOUT", + "DIVIDEND_REINVEST", + "SEGREGATION", + "STAMP_DUTY_TAX", + "TDS_TAX", + "STT_TAX", + "MISC", + "REVERSAL", + "UNKNOWN", + ] + ] = None + """Transaction type. + + Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, + SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, + DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, + REVERSAL, UNKNOWN. + """ + + units: Optional[float] = None + """Number of units involved in transaction""" + + class DematAccountHoldingsAif(BaseModel): - additional_info: Optional[object] = None + additional_info: Optional[DematAccountHoldingsAifAdditionalInfo] = None """Additional information specific to the AIF""" isin: Optional[str] = None @@ -80,6 +191,9 @@ class DematAccountHoldingsAif(BaseModel): name: Optional[str] = None """Name of the AIF""" + transactions: Optional[List[DematAccountHoldingsAifTransaction]] = None + """List of transactions for this holding (beta)""" + units: Optional[float] = None """Number of units held""" @@ -87,8 +201,101 @@ class DematAccountHoldingsAif(BaseModel): """Current market value of the holding""" +class DematAccountHoldingsCorporateBondAdditionalInfo(BaseModel): + """Additional information specific to the corporate bond""" + + close_units: Optional[float] = None + """Closing balance units for the statement period (beta)""" + + open_units: Optional[float] = None + """Opening balance units for the statement period (beta)""" + + +class DematAccountHoldingsCorporateBondTransactionAdditionalInfo(BaseModel): + """Additional transaction-specific fields that vary by source""" + + capital_withdrawal: Optional[float] = None + """Capital withdrawal amount (CDSL MF transactions)""" + + credit: Optional[float] = None + """Units credited (demat transactions)""" + + debit: Optional[float] = None + """Units debited (demat transactions)""" + + income_distribution: Optional[float] = None + """Income distribution amount (CDSL MF transactions)""" + + order_no: Optional[str] = None + """Order/transaction reference number (demat transactions)""" + + price: Optional[float] = None + """Price per unit (NSDL/CDSL MF transactions)""" + + stamp_duty: Optional[float] = None + """Stamp duty charged""" + + +class DematAccountHoldingsCorporateBondTransaction(BaseModel): + """ + Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) + """ + + additional_info: Optional[DematAccountHoldingsCorporateBondTransactionAdditionalInfo] = None + """Additional transaction-specific fields that vary by source""" + + amount: Optional[float] = None + """Transaction amount in currency (computed from units × price/NAV)""" + + balance: Optional[float] = None + """Balance units after transaction""" + + date: Optional[datetime.date] = None + """Transaction date (YYYY-MM-DD)""" + + description: Optional[str] = None + """Transaction description/particulars""" + + dividend_rate: Optional[float] = None + """Dividend rate (for DIVIDEND_PAYOUT transactions)""" + + nav: Optional[float] = None + """NAV/price per unit on transaction date""" + + type: Optional[ + Literal[ + "PURCHASE", + "PURCHASE_SIP", + "REDEMPTION", + "SWITCH_IN", + "SWITCH_IN_MERGER", + "SWITCH_OUT", + "SWITCH_OUT_MERGER", + "DIVIDEND_PAYOUT", + "DIVIDEND_REINVEST", + "SEGREGATION", + "STAMP_DUTY_TAX", + "TDS_TAX", + "STT_TAX", + "MISC", + "REVERSAL", + "UNKNOWN", + ] + ] = None + """Transaction type. + + Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, + SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, + DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, + REVERSAL, UNKNOWN. + """ + + units: Optional[float] = None + """Number of units involved in transaction""" + + class DematAccountHoldingsCorporateBond(BaseModel): - additional_info: Optional[object] = None + additional_info: Optional[DematAccountHoldingsCorporateBondAdditionalInfo] = None """Additional information specific to the corporate bond""" isin: Optional[str] = None @@ -97,6 +304,9 @@ class DematAccountHoldingsCorporateBond(BaseModel): name: Optional[str] = None """Name of the corporate bond""" + transactions: Optional[List[DematAccountHoldingsCorporateBondTransaction]] = None + """List of transactions for this holding (beta)""" + units: Optional[float] = None """Number of units held""" @@ -104,8 +314,101 @@ class DematAccountHoldingsCorporateBond(BaseModel): """Current market value of the holding""" +class DematAccountHoldingsDematMutualFundAdditionalInfo(BaseModel): + """Additional information specific to the mutual fund""" + + close_units: Optional[float] = None + """Closing balance units for the statement period (beta)""" + + open_units: Optional[float] = None + """Opening balance units for the statement period (beta)""" + + +class DematAccountHoldingsDematMutualFundTransactionAdditionalInfo(BaseModel): + """Additional transaction-specific fields that vary by source""" + + capital_withdrawal: Optional[float] = None + """Capital withdrawal amount (CDSL MF transactions)""" + + credit: Optional[float] = None + """Units credited (demat transactions)""" + + debit: Optional[float] = None + """Units debited (demat transactions)""" + + income_distribution: Optional[float] = None + """Income distribution amount (CDSL MF transactions)""" + + order_no: Optional[str] = None + """Order/transaction reference number (demat transactions)""" + + price: Optional[float] = None + """Price per unit (NSDL/CDSL MF transactions)""" + + stamp_duty: Optional[float] = None + """Stamp duty charged""" + + +class DematAccountHoldingsDematMutualFundTransaction(BaseModel): + """ + Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) + """ + + additional_info: Optional[DematAccountHoldingsDematMutualFundTransactionAdditionalInfo] = None + """Additional transaction-specific fields that vary by source""" + + amount: Optional[float] = None + """Transaction amount in currency (computed from units × price/NAV)""" + + balance: Optional[float] = None + """Balance units after transaction""" + + date: Optional[datetime.date] = None + """Transaction date (YYYY-MM-DD)""" + + description: Optional[str] = None + """Transaction description/particulars""" + + dividend_rate: Optional[float] = None + """Dividend rate (for DIVIDEND_PAYOUT transactions)""" + + nav: Optional[float] = None + """NAV/price per unit on transaction date""" + + type: Optional[ + Literal[ + "PURCHASE", + "PURCHASE_SIP", + "REDEMPTION", + "SWITCH_IN", + "SWITCH_IN_MERGER", + "SWITCH_OUT", + "SWITCH_OUT_MERGER", + "DIVIDEND_PAYOUT", + "DIVIDEND_REINVEST", + "SEGREGATION", + "STAMP_DUTY_TAX", + "TDS_TAX", + "STT_TAX", + "MISC", + "REVERSAL", + "UNKNOWN", + ] + ] = None + """Transaction type. + + Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, + SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, + DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, + REVERSAL, UNKNOWN. + """ + + units: Optional[float] = None + """Number of units involved in transaction""" + + class DematAccountHoldingsDematMutualFund(BaseModel): - additional_info: Optional[object] = None + additional_info: Optional[DematAccountHoldingsDematMutualFundAdditionalInfo] = None """Additional information specific to the mutual fund""" isin: Optional[str] = None @@ -114,6 +417,9 @@ class DematAccountHoldingsDematMutualFund(BaseModel): name: Optional[str] = None """Name of the mutual fund""" + transactions: Optional[List[DematAccountHoldingsDematMutualFundTransaction]] = None + """List of transactions for this holding (beta)""" + units: Optional[float] = None """Number of units held""" @@ -121,8 +427,101 @@ class DematAccountHoldingsDematMutualFund(BaseModel): """Current market value of the holding""" +class DematAccountHoldingsEquityAdditionalInfo(BaseModel): + """Additional information specific to the equity""" + + close_units: Optional[float] = None + """Closing balance units for the statement period (beta)""" + + open_units: Optional[float] = None + """Opening balance units for the statement period (beta)""" + + +class DematAccountHoldingsEquityTransactionAdditionalInfo(BaseModel): + """Additional transaction-specific fields that vary by source""" + + capital_withdrawal: Optional[float] = None + """Capital withdrawal amount (CDSL MF transactions)""" + + credit: Optional[float] = None + """Units credited (demat transactions)""" + + debit: Optional[float] = None + """Units debited (demat transactions)""" + + income_distribution: Optional[float] = None + """Income distribution amount (CDSL MF transactions)""" + + order_no: Optional[str] = None + """Order/transaction reference number (demat transactions)""" + + price: Optional[float] = None + """Price per unit (NSDL/CDSL MF transactions)""" + + stamp_duty: Optional[float] = None + """Stamp duty charged""" + + +class DematAccountHoldingsEquityTransaction(BaseModel): + """ + Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) + """ + + additional_info: Optional[DematAccountHoldingsEquityTransactionAdditionalInfo] = None + """Additional transaction-specific fields that vary by source""" + + amount: Optional[float] = None + """Transaction amount in currency (computed from units × price/NAV)""" + + balance: Optional[float] = None + """Balance units after transaction""" + + date: Optional[datetime.date] = None + """Transaction date (YYYY-MM-DD)""" + + description: Optional[str] = None + """Transaction description/particulars""" + + dividend_rate: Optional[float] = None + """Dividend rate (for DIVIDEND_PAYOUT transactions)""" + + nav: Optional[float] = None + """NAV/price per unit on transaction date""" + + type: Optional[ + Literal[ + "PURCHASE", + "PURCHASE_SIP", + "REDEMPTION", + "SWITCH_IN", + "SWITCH_IN_MERGER", + "SWITCH_OUT", + "SWITCH_OUT_MERGER", + "DIVIDEND_PAYOUT", + "DIVIDEND_REINVEST", + "SEGREGATION", + "STAMP_DUTY_TAX", + "TDS_TAX", + "STT_TAX", + "MISC", + "REVERSAL", + "UNKNOWN", + ] + ] = None + """Transaction type. + + Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, + SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, + DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, + REVERSAL, UNKNOWN. + """ + + units: Optional[float] = None + """Number of units involved in transaction""" + + class DematAccountHoldingsEquity(BaseModel): - additional_info: Optional[object] = None + additional_info: Optional[DematAccountHoldingsEquityAdditionalInfo] = None """Additional information specific to the equity""" isin: Optional[str] = None @@ -131,6 +530,9 @@ class DematAccountHoldingsEquity(BaseModel): name: Optional[str] = None """Name of the equity""" + transactions: Optional[List[DematAccountHoldingsEquityTransaction]] = None + """List of transactions for this holding (beta)""" + units: Optional[float] = None """Number of units held""" @@ -138,8 +540,101 @@ class DematAccountHoldingsEquity(BaseModel): """Current market value of the holding""" +class DematAccountHoldingsGovernmentSecurityAdditionalInfo(BaseModel): + """Additional information specific to the government security""" + + close_units: Optional[float] = None + """Closing balance units for the statement period (beta)""" + + open_units: Optional[float] = None + """Opening balance units for the statement period (beta)""" + + +class DematAccountHoldingsGovernmentSecurityTransactionAdditionalInfo(BaseModel): + """Additional transaction-specific fields that vary by source""" + + capital_withdrawal: Optional[float] = None + """Capital withdrawal amount (CDSL MF transactions)""" + + credit: Optional[float] = None + """Units credited (demat transactions)""" + + debit: Optional[float] = None + """Units debited (demat transactions)""" + + income_distribution: Optional[float] = None + """Income distribution amount (CDSL MF transactions)""" + + order_no: Optional[str] = None + """Order/transaction reference number (demat transactions)""" + + price: Optional[float] = None + """Price per unit (NSDL/CDSL MF transactions)""" + + stamp_duty: Optional[float] = None + """Stamp duty charged""" + + +class DematAccountHoldingsGovernmentSecurityTransaction(BaseModel): + """ + Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) + """ + + additional_info: Optional[DematAccountHoldingsGovernmentSecurityTransactionAdditionalInfo] = None + """Additional transaction-specific fields that vary by source""" + + amount: Optional[float] = None + """Transaction amount in currency (computed from units × price/NAV)""" + + balance: Optional[float] = None + """Balance units after transaction""" + + date: Optional[datetime.date] = None + """Transaction date (YYYY-MM-DD)""" + + description: Optional[str] = None + """Transaction description/particulars""" + + dividend_rate: Optional[float] = None + """Dividend rate (for DIVIDEND_PAYOUT transactions)""" + + nav: Optional[float] = None + """NAV/price per unit on transaction date""" + + type: Optional[ + Literal[ + "PURCHASE", + "PURCHASE_SIP", + "REDEMPTION", + "SWITCH_IN", + "SWITCH_IN_MERGER", + "SWITCH_OUT", + "SWITCH_OUT_MERGER", + "DIVIDEND_PAYOUT", + "DIVIDEND_REINVEST", + "SEGREGATION", + "STAMP_DUTY_TAX", + "TDS_TAX", + "STT_TAX", + "MISC", + "REVERSAL", + "UNKNOWN", + ] + ] = None + """Transaction type. + + Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, + SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, + DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, + REVERSAL, UNKNOWN. + """ + + units: Optional[float] = None + """Number of units involved in transaction""" + + class DematAccountHoldingsGovernmentSecurity(BaseModel): - additional_info: Optional[object] = None + additional_info: Optional[DematAccountHoldingsGovernmentSecurityAdditionalInfo] = None """Additional information specific to the government security""" isin: Optional[str] = None @@ -148,6 +643,9 @@ class DematAccountHoldingsGovernmentSecurity(BaseModel): name: Optional[str] = None """Name of the government security""" + transactions: Optional[List[DematAccountHoldingsGovernmentSecurityTransaction]] = None + """List of transactions for this holding (beta)""" + units: Optional[float] = None """Number of units held""" @@ -278,6 +776,8 @@ class Meta(BaseModel): class MutualFundAdditionalInfo(BaseModel): + """Additional folio information""" + kyc: Optional[str] = None """KYC status of the folio""" @@ -297,6 +797,8 @@ class MutualFundLinkedHolder(BaseModel): class MutualFundSchemeAdditionalInfo(BaseModel): + """Additional information specific to the scheme""" + advisor: Optional[str] = None """Financial advisor name (CAMS/KFintech)""" @@ -304,10 +806,10 @@ class MutualFundSchemeAdditionalInfo(BaseModel): """AMFI code for the scheme (CAMS/KFintech)""" close_units: Optional[float] = None - """Closing balance units (CAMS/KFintech)""" + """Closing balance units for the statement period""" open_units: Optional[float] = None - """Opening balance units (CAMS/KFintech)""" + """Opening balance units for the statement period""" rta_code: Optional[str] = None """RTA code for the scheme (CAMS/KFintech)""" @@ -321,36 +823,87 @@ class MutualFundSchemeGain(BaseModel): """Percentage gain or loss""" +class MutualFundSchemeTransactionAdditionalInfo(BaseModel): + """Additional transaction-specific fields that vary by source""" + + capital_withdrawal: Optional[float] = None + """Capital withdrawal amount (CDSL MF transactions)""" + + credit: Optional[float] = None + """Units credited (demat transactions)""" + + debit: Optional[float] = None + """Units debited (demat transactions)""" + + income_distribution: Optional[float] = None + """Income distribution amount (CDSL MF transactions)""" + + order_no: Optional[str] = None + """Order/transaction reference number (demat transactions)""" + + price: Optional[float] = None + """Price per unit (NSDL/CDSL MF transactions)""" + + stamp_duty: Optional[float] = None + """Stamp duty charged""" + + class MutualFundSchemeTransaction(BaseModel): + """ + Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) + """ + + additional_info: Optional[MutualFundSchemeTransactionAdditionalInfo] = None + """Additional transaction-specific fields that vary by source""" + amount: Optional[float] = None - """Transaction amount""" + """Transaction amount in currency (computed from units × price/NAV)""" balance: Optional[float] = None """Balance units after transaction""" date: Optional[datetime.date] = None - """Transaction date""" + """Transaction date (YYYY-MM-DD)""" description: Optional[str] = None - """Transaction description""" + """Transaction description/particulars""" dividend_rate: Optional[float] = None - """Dividend rate (for dividend transactions)""" + """Dividend rate (for DIVIDEND_PAYOUT transactions)""" nav: Optional[float] = None - """NAV on transaction date""" - - type: Optional[str] = None - """Transaction type detected based on description. - - Possible values are - PURCHASE,PURCHASE_SIP,REDEMPTION,SWITCH_IN,SWITCH_IN_MERGER,SWITCH_OUT,SWITCH_OUT_MERGER,DIVIDEND_PAYOUT,DIVIDEND_REINVESTMENT,SEGREGATION,STAMP_DUTY_TAX,TDS_TAX,STT_TAX,MISC. - If dividend_rate is present, then possible values are dividend_rate is - applicable only for DIVIDEND_PAYOUT and DIVIDEND_REINVESTMENT. + """NAV/price per unit on transaction date""" + + type: Optional[ + Literal[ + "PURCHASE", + "PURCHASE_SIP", + "REDEMPTION", + "SWITCH_IN", + "SWITCH_IN_MERGER", + "SWITCH_OUT", + "SWITCH_OUT_MERGER", + "DIVIDEND_PAYOUT", + "DIVIDEND_REINVEST", + "SEGREGATION", + "STAMP_DUTY_TAX", + "TDS_TAX", + "STT_TAX", + "MISC", + "REVERSAL", + "UNKNOWN", + ] + ] = None + """Transaction type. + + Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, + SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, + DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, + REVERSAL, UNKNOWN. """ units: Optional[float] = None - """Number of units involved""" + """Number of units involved in transaction""" class MutualFundScheme(BaseModel): @@ -409,6 +962,8 @@ class MutualFund(BaseModel): class NpFundAdditionalInfo(BaseModel): + """Additional information specific to the NPS fund""" + manager: Optional[str] = None """Fund manager name""" From e4b92a6d6a01fcc1551da0ab3a12fa1c8de3b433 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 02:17:29 +0000 Subject: [PATCH 037/116] feat(api): api update --- .stats.yml | 6 +- api.md | 12 - src/cas_parser/_client.py | 39 +-- src/cas_parser/resources/__init__.py | 14 -- src/cas_parser/resources/cas_generator.py | 225 ------------------ src/cas_parser/types/__init__.py | 2 - .../cas_generator_generate_cas_params.py | 30 --- .../cas_generator_generate_cas_response.py | 13 - tests/api_resources/test_cas_generator.py | 136 ----------- 9 files changed, 4 insertions(+), 473 deletions(-) delete mode 100644 src/cas_parser/resources/cas_generator.py delete mode 100644 src/cas_parser/types/cas_generator_generate_cas_params.py delete mode 100644 src/cas_parser/types/cas_generator_generate_cas_response.py delete mode 100644 tests/api_resources/test_cas_generator.py diff --git a/.stats.yml b/.stats.yml index 48b33b3..4465de7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 5 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-38618cc5c938e87eeacf4893d6a6ba4e6ef7da390e6283dc7b50b484a7b97165.yml -openapi_spec_hash: b9e439ecee904ded01aa34efdee88856 +configured_endpoints: 4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-7e6397bddc220d1a59b5e2c7e7c3ff38f1a6eb174f4e383e03bc49cf78c8c44f.yml +openapi_spec_hash: cb852eeb4ce89c80f4246815cbe21f72 config_hash: cb5d75abef6264b5d86448caf7295afa diff --git a/api.md b/api.md index 9f56f41..7a55253 100644 --- a/api.md +++ b/api.md @@ -12,15 +12,3 @@ Methods: - client.cas_parser.cdsl(\*\*params) -> UnifiedResponse - client.cas_parser.nsdl(\*\*params) -> UnifiedResponse - client.cas_parser.smart_parse(\*\*params) -> UnifiedResponse - -# CasGenerator - -Types: - -```python -from cas_parser.types import CasGeneratorGenerateCasResponse -``` - -Methods: - -- client.cas_generator.generate_cas(\*\*params) -> CasGeneratorGenerateCasResponse diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py index f0d7ed2..b84a489 100644 --- a/src/cas_parser/_client.py +++ b/src/cas_parser/_client.py @@ -31,9 +31,8 @@ ) if TYPE_CHECKING: - from .resources import cas_parser, cas_generator + from .resources import cas_parser from .resources.cas_parser import CasParserResource, AsyncCasParserResource - from .resources.cas_generator import CasGeneratorResource, AsyncCasGeneratorResource __all__ = [ "Timeout", @@ -108,12 +107,6 @@ def cas_parser(self) -> CasParserResource: return CasParserResource(self) - @cached_property - def cas_generator(self) -> CasGeneratorResource: - from .resources.cas_generator import CasGeneratorResource - - return CasGeneratorResource(self) - @cached_property def with_raw_response(self) -> CasParserWithRawResponse: return CasParserWithRawResponse(self) @@ -288,12 +281,6 @@ def cas_parser(self) -> AsyncCasParserResource: return AsyncCasParserResource(self) - @cached_property - def cas_generator(self) -> AsyncCasGeneratorResource: - from .resources.cas_generator import AsyncCasGeneratorResource - - return AsyncCasGeneratorResource(self) - @cached_property def with_raw_response(self) -> AsyncCasParserWithRawResponse: return AsyncCasParserWithRawResponse(self) @@ -419,12 +406,6 @@ def cas_parser(self) -> cas_parser.CasParserResourceWithRawResponse: return CasParserResourceWithRawResponse(self._client.cas_parser) - @cached_property - def cas_generator(self) -> cas_generator.CasGeneratorResourceWithRawResponse: - from .resources.cas_generator import CasGeneratorResourceWithRawResponse - - return CasGeneratorResourceWithRawResponse(self._client.cas_generator) - class AsyncCasParserWithRawResponse: _client: AsyncCasParser @@ -438,12 +419,6 @@ def cas_parser(self) -> cas_parser.AsyncCasParserResourceWithRawResponse: return AsyncCasParserResourceWithRawResponse(self._client.cas_parser) - @cached_property - def cas_generator(self) -> cas_generator.AsyncCasGeneratorResourceWithRawResponse: - from .resources.cas_generator import AsyncCasGeneratorResourceWithRawResponse - - return AsyncCasGeneratorResourceWithRawResponse(self._client.cas_generator) - class CasParserWithStreamedResponse: _client: CasParser @@ -457,12 +432,6 @@ def cas_parser(self) -> cas_parser.CasParserResourceWithStreamingResponse: return CasParserResourceWithStreamingResponse(self._client.cas_parser) - @cached_property - def cas_generator(self) -> cas_generator.CasGeneratorResourceWithStreamingResponse: - from .resources.cas_generator import CasGeneratorResourceWithStreamingResponse - - return CasGeneratorResourceWithStreamingResponse(self._client.cas_generator) - class AsyncCasParserWithStreamedResponse: _client: AsyncCasParser @@ -476,12 +445,6 @@ def cas_parser(self) -> cas_parser.AsyncCasParserResourceWithStreamingResponse: return AsyncCasParserResourceWithStreamingResponse(self._client.cas_parser) - @cached_property - def cas_generator(self) -> cas_generator.AsyncCasGeneratorResourceWithStreamingResponse: - from .resources.cas_generator import AsyncCasGeneratorResourceWithStreamingResponse - - return AsyncCasGeneratorResourceWithStreamingResponse(self._client.cas_generator) - Client = CasParser diff --git a/src/cas_parser/resources/__init__.py b/src/cas_parser/resources/__init__.py index f1bb2bf..5da0162 100644 --- a/src/cas_parser/resources/__init__.py +++ b/src/cas_parser/resources/__init__.py @@ -8,14 +8,6 @@ CasParserResourceWithStreamingResponse, AsyncCasParserResourceWithStreamingResponse, ) -from .cas_generator import ( - CasGeneratorResource, - AsyncCasGeneratorResource, - CasGeneratorResourceWithRawResponse, - AsyncCasGeneratorResourceWithRawResponse, - CasGeneratorResourceWithStreamingResponse, - AsyncCasGeneratorResourceWithStreamingResponse, -) __all__ = [ "CasParserResource", @@ -24,10 +16,4 @@ "AsyncCasParserResourceWithRawResponse", "CasParserResourceWithStreamingResponse", "AsyncCasParserResourceWithStreamingResponse", - "CasGeneratorResource", - "AsyncCasGeneratorResource", - "CasGeneratorResourceWithRawResponse", - "AsyncCasGeneratorResourceWithRawResponse", - "CasGeneratorResourceWithStreamingResponse", - "AsyncCasGeneratorResourceWithStreamingResponse", ] diff --git a/src/cas_parser/resources/cas_generator.py b/src/cas_parser/resources/cas_generator.py deleted file mode 100644 index 26ff32a..0000000 --- a/src/cas_parser/resources/cas_generator.py +++ /dev/null @@ -1,225 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal - -import httpx - -from ..types import cas_generator_generate_cas_params -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.cas_generator_generate_cas_response import CasGeneratorGenerateCasResponse - -__all__ = ["CasGeneratorResource", "AsyncCasGeneratorResource"] - - -class CasGeneratorResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> CasGeneratorResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return CasGeneratorResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> CasGeneratorResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return CasGeneratorResourceWithStreamingResponse(self) - - def generate_cas( - self, - *, - email: str, - from_date: str, - password: str, - to_date: str, - cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] | Omit = omit, - pan_no: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> CasGeneratorGenerateCasResponse: - """ - This endpoint generates CAS (Consolidated Account Statement) documents by - submitting a mailback request to the specified CAS authority. Currently only - supports KFintech, with plans to support CAMS, CDSL, and NSDL in the future. - - Args: - email: Email address to receive the CAS document - - from_date: Start date for the CAS period (format YYYY-MM-DD) - - password: Password to protect the generated CAS PDF - - to_date: End date for the CAS period (format YYYY-MM-DD) - - cas_authority: CAS authority to generate the document from (currently only kfintech is - supported) - - pan_no: PAN number (optional for some CAS authorities) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/v4/generate", - body=maybe_transform( - { - "email": email, - "from_date": from_date, - "password": password, - "to_date": to_date, - "cas_authority": cas_authority, - "pan_no": pan_no, - }, - cas_generator_generate_cas_params.CasGeneratorGenerateCasParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=CasGeneratorGenerateCasResponse, - ) - - -class AsyncCasGeneratorResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncCasGeneratorResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return AsyncCasGeneratorResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncCasGeneratorResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return AsyncCasGeneratorResourceWithStreamingResponse(self) - - async def generate_cas( - self, - *, - email: str, - from_date: str, - password: str, - to_date: str, - cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] | Omit = omit, - pan_no: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> CasGeneratorGenerateCasResponse: - """ - This endpoint generates CAS (Consolidated Account Statement) documents by - submitting a mailback request to the specified CAS authority. Currently only - supports KFintech, with plans to support CAMS, CDSL, and NSDL in the future. - - Args: - email: Email address to receive the CAS document - - from_date: Start date for the CAS period (format YYYY-MM-DD) - - password: Password to protect the generated CAS PDF - - to_date: End date for the CAS period (format YYYY-MM-DD) - - cas_authority: CAS authority to generate the document from (currently only kfintech is - supported) - - pan_no: PAN number (optional for some CAS authorities) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/v4/generate", - body=await async_maybe_transform( - { - "email": email, - "from_date": from_date, - "password": password, - "to_date": to_date, - "cas_authority": cas_authority, - "pan_no": pan_no, - }, - cas_generator_generate_cas_params.CasGeneratorGenerateCasParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=CasGeneratorGenerateCasResponse, - ) - - -class CasGeneratorResourceWithRawResponse: - def __init__(self, cas_generator: CasGeneratorResource) -> None: - self._cas_generator = cas_generator - - self.generate_cas = to_raw_response_wrapper( - cas_generator.generate_cas, - ) - - -class AsyncCasGeneratorResourceWithRawResponse: - def __init__(self, cas_generator: AsyncCasGeneratorResource) -> None: - self._cas_generator = cas_generator - - self.generate_cas = async_to_raw_response_wrapper( - cas_generator.generate_cas, - ) - - -class CasGeneratorResourceWithStreamingResponse: - def __init__(self, cas_generator: CasGeneratorResource) -> None: - self._cas_generator = cas_generator - - self.generate_cas = to_streamed_response_wrapper( - cas_generator.generate_cas, - ) - - -class AsyncCasGeneratorResourceWithStreamingResponse: - def __init__(self, cas_generator: AsyncCasGeneratorResource) -> None: - self._cas_generator = cas_generator - - self.generate_cas = async_to_streamed_response_wrapper( - cas_generator.generate_cas, - ) diff --git a/src/cas_parser/types/__init__.py b/src/cas_parser/types/__init__.py index 4dbdba1..fcdbc0b 100644 --- a/src/cas_parser/types/__init__.py +++ b/src/cas_parser/types/__init__.py @@ -7,5 +7,3 @@ from .cas_parser_nsdl_params import CasParserNsdlParams as CasParserNsdlParams from .cas_parser_smart_parse_params import CasParserSmartParseParams as CasParserSmartParseParams from .cas_parser_cams_kfintech_params import CasParserCamsKfintechParams as CasParserCamsKfintechParams -from .cas_generator_generate_cas_params import CasGeneratorGenerateCasParams as CasGeneratorGenerateCasParams -from .cas_generator_generate_cas_response import CasGeneratorGenerateCasResponse as CasGeneratorGenerateCasResponse diff --git a/src/cas_parser/types/cas_generator_generate_cas_params.py b/src/cas_parser/types/cas_generator_generate_cas_params.py deleted file mode 100644 index 253dcea..0000000 --- a/src/cas_parser/types/cas_generator_generate_cas_params.py +++ /dev/null @@ -1,30 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, TypedDict - -__all__ = ["CasGeneratorGenerateCasParams"] - - -class CasGeneratorGenerateCasParams(TypedDict, total=False): - email: Required[str] - """Email address to receive the CAS document""" - - from_date: Required[str] - """Start date for the CAS period (format YYYY-MM-DD)""" - - password: Required[str] - """Password to protect the generated CAS PDF""" - - to_date: Required[str] - """End date for the CAS period (format YYYY-MM-DD)""" - - cas_authority: Literal["kfintech", "cams", "cdsl", "nsdl"] - """ - CAS authority to generate the document from (currently only kfintech is - supported) - """ - - pan_no: str - """PAN number (optional for some CAS authorities)""" diff --git a/src/cas_parser/types/cas_generator_generate_cas_response.py b/src/cas_parser/types/cas_generator_generate_cas_response.py deleted file mode 100644 index e781ef9..0000000 --- a/src/cas_parser/types/cas_generator_generate_cas_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["CasGeneratorGenerateCasResponse"] - - -class CasGeneratorGenerateCasResponse(BaseModel): - msg: Optional[str] = None - - status: Optional[str] = None diff --git a/tests/api_resources/test_cas_generator.py b/tests/api_resources/test_cas_generator.py deleted file mode 100644 index d0d591d..0000000 --- a/tests/api_resources/test_cas_generator.py +++ /dev/null @@ -1,136 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from cas_parser import CasParser, AsyncCasParser -from tests.utils import assert_matches_type -from cas_parser.types import CasGeneratorGenerateCasResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestCasGenerator: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_generate_cas(self, client: CasParser) -> None: - cas_generator = client.cas_generator.generate_cas( - email="user@example.com", - from_date="2023-01-01", - password="Abcdefghi12$", - to_date="2023-12-31", - ) - assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_generate_cas_with_all_params(self, client: CasParser) -> None: - cas_generator = client.cas_generator.generate_cas( - email="user@example.com", - from_date="2023-01-01", - password="Abcdefghi12$", - to_date="2023-12-31", - cas_authority="kfintech", - pan_no="ABCDE1234F", - ) - assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_generate_cas(self, client: CasParser) -> None: - response = client.cas_generator.with_raw_response.generate_cas( - email="user@example.com", - from_date="2023-01-01", - password="Abcdefghi12$", - to_date="2023-12-31", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - cas_generator = response.parse() - assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_generate_cas(self, client: CasParser) -> None: - with client.cas_generator.with_streaming_response.generate_cas( - email="user@example.com", - from_date="2023-01-01", - password="Abcdefghi12$", - to_date="2023-12-31", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - cas_generator = response.parse() - assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncCasGenerator: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_generate_cas(self, async_client: AsyncCasParser) -> None: - cas_generator = await async_client.cas_generator.generate_cas( - email="user@example.com", - from_date="2023-01-01", - password="Abcdefghi12$", - to_date="2023-12-31", - ) - assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_generate_cas_with_all_params(self, async_client: AsyncCasParser) -> None: - cas_generator = await async_client.cas_generator.generate_cas( - email="user@example.com", - from_date="2023-01-01", - password="Abcdefghi12$", - to_date="2023-12-31", - cas_authority="kfintech", - pan_no="ABCDE1234F", - ) - assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_generate_cas(self, async_client: AsyncCasParser) -> None: - response = await async_client.cas_generator.with_raw_response.generate_cas( - email="user@example.com", - from_date="2023-01-01", - password="Abcdefghi12$", - to_date="2023-12-31", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - cas_generator = await response.parse() - assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_generate_cas(self, async_client: AsyncCasParser) -> None: - async with async_client.cas_generator.with_streaming_response.generate_cas( - email="user@example.com", - from_date="2023-01-01", - password="Abcdefghi12$", - to_date="2023-12-31", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - cas_generator = await response.parse() - assert_matches_type(CasGeneratorGenerateCasResponse, cas_generator, path=["response"]) - - assert cast(Any, response.is_closed) is True From 80803f9598ce4028b93d720334a4cb9e1b48a74f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 02:17:31 +0000 Subject: [PATCH 038/116] feat(api): api update --- .github/workflows/ci.yml | 6 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- .stats.yml | 4 +- README.md | 9 ++ src/cas_parser/_base_client.py | 145 +++++++++++++++++++-- src/cas_parser/_models.py | 17 ++- src/cas_parser/_types.py | 9 ++ tests/test_client.py | 187 ++++++++++++++++++++++++++- 9 files changed, 360 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a9eb32..e945b15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index f0a5b3c..9a3087b 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index ea04f96..a77924a 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'CASParser/cas-parser-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | diff --git a/.stats.yml b/.stats.yml index 4465de7..968a0a4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-7e6397bddc220d1a59b5e2c7e7c3ff38f1a6eb174f4e383e03bc49cf78c8c44f.yml -openapi_spec_hash: cb852eeb4ce89c80f4246815cbe21f72 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-ce2296c4b14d27c141bb2745607d2456c923fdca3ae0a0a0800c26e564333850.yml +openapi_spec_hash: 8eb586ccf16b534c0c15ff6a22274c7d config_hash: cb5d75abef6264b5d86448caf7295afa diff --git a/README.md b/README.md index bfab47b..d3f8ab4 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,15 @@ and offers both synchronous and asynchronous clients powered by [httpx](https:// It is generated with [Stainless](https://www.stainless.com/). +## MCP Server + +Use the Cas Parser MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. + +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=cas-parser-node-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNhcy1wYXJzZXItbm9kZS1tY3AiXX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-node-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22cas-parser-node-mcp%22%5D%7D) + +> Note: You may need to set environment variables in your MCP client. + ## Documentation The REST API documentation can be found on [docs.casparser.in](https://docs.casparser.in/reference). The full API of this library can be found in [api.md](api.md). diff --git a/src/cas_parser/_base_client.py b/src/cas_parser/_base_client.py index 9cfe0c2..da01b6f 100644 --- a/src/cas_parser/_base_client.py +++ b/src/cas_parser/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1282,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1717,6 +1786,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1729,6 +1799,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1742,6 +1813,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1754,13 +1826,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1770,11 +1854,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1784,11 +1885,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1798,9 +1911,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/cas_parser/_models.py b/src/cas_parser/_models.py index ca9500b..29070e0 100644 --- a/src/cas_parser/_models.py +++ b/src/cas_parser/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/cas_parser/_types.py b/src/cas_parser/_types.py index 2c15258..3f7802c 100644 --- a/src/cas_parser/_types.py +++ b/src/cas_parser/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/tests/test_client.py b/tests/test_client.py index 47523fb..6f70fd4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" @@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: CasParser | AsyncCasParser) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -502,6 +555,70 @@ def test_multipart_repeating_array(self, client: CasParser) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: CasParser) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with CasParser( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: CasParser) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: CasParser) -> None: class Model1(BaseModel): @@ -1321,6 +1438,72 @@ def test_multipart_repeating_array(self, async_client: AsyncCasParser) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncCasParser( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncCasParser + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncCasParser) -> None: class Model1(BaseModel): From 8484d38da1855ee765e3222b0dc6a4ca2caa2e3a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 04:17:22 +0000 Subject: [PATCH 039/116] feat(api): api update --- .github/workflows/ci.yml | 2 +- .stats.yml | 4 +- README.md | 4 +- src/cas_parser/_base_client.py | 7 +- src/cas_parser/_compat.py | 6 +- src/cas_parser/_utils/_json.py | 35 +++++++++ tests/test_utils/test_json.py | 126 +++++++++++++++++++++++++++++++++ 7 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 src/cas_parser/_utils/_json.py create mode 100644 tests/test_utils/test_json.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e945b15..06d6959 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/cas-parser-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); diff --git a/.stats.yml b/.stats.yml index 968a0a4..9cc4732 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-ce2296c4b14d27c141bb2745607d2456c923fdca3ae0a0a0800c26e564333850.yml -openapi_spec_hash: 8eb586ccf16b534c0c15ff6a22274c7d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-2e3df9c77e887f49ca3dffd5d68f30a8a0ea0b557f31282dd191ce85713e3e34.yml +openapi_spec_hash: 1cb90023118602a40a106cd51ed6a926 config_hash: cb5d75abef6264b5d86448caf7295afa diff --git a/README.md b/README.md index d3f8ab4..6ef2f56 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/). Use the Cas Parser MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=cas-parser-node-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNhcy1wYXJzZXItbm9kZS1tY3AiXX0) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-node-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22cas-parser-node-mcp%22%5D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=cas-parser-node-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNhcy1wYXJzZXItbm9kZS1tY3AiXSwiZW52Ijp7IkNBU19QQVJTRVJfQVBJX0tFWSI6Ik15IEFQSSBLZXkifX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-node-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22cas-parser-node-mcp%22%5D%2C%22env%22%3A%7B%22CAS_PARSER_API_KEY%22%3A%22My%20API%20Key%22%7D%7D) > Note: You may need to set environment variables in your MCP client. diff --git a/src/cas_parser/_base_client.py b/src/cas_parser/_base_client.py index da01b6f..9937027 100644 --- a/src/cas_parser/_base_client.py +++ b/src/cas_parser/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/cas_parser/_compat.py b/src/cas_parser/_compat.py index bdef67f..786ff42 100644 --- a/src/cas_parser/_compat.py +++ b/src/cas_parser/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/cas_parser/_utils/_json.py b/src/cas_parser/_utils/_json.py new file mode 100644 index 0000000..6058421 --- /dev/null +++ b/src/cas_parser/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 0000000..902417f --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from cas_parser import _compat +from cas_parser._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' From 1a2da97a144354dd9f324fff2e8edb9f35c3bcbf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:17:22 +0000 Subject: [PATCH 040/116] feat(api): api update --- .stats.yml | 4 +-- src/cas_parser/resources/cas_parser.py | 32 +++++++++---------- .../types/cas_parser_cams_kfintech_params.py | 4 +-- .../types/cas_parser_cdsl_params.py | 4 +-- .../types/cas_parser_nsdl_params.py | 4 +-- .../types/cas_parser_smart_parse_params.py | 4 +-- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9cc4732..2c90fa6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-2e3df9c77e887f49ca3dffd5d68f30a8a0ea0b557f31282dd191ce85713e3e34.yml -openapi_spec_hash: 1cb90023118602a40a106cd51ed6a926 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml +openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f config_hash: cb5d75abef6264b5d86448caf7295afa diff --git a/src/cas_parser/resources/cas_parser.py b/src/cas_parser/resources/cas_parser.py index e82b0e9..3a770a9 100644 --- a/src/cas_parser/resources/cas_parser.py +++ b/src/cas_parser/resources/cas_parser.py @@ -69,9 +69,9 @@ def cams_kfintech( Args: password: Password for the PDF file (if required) - pdf_file: Base64 encoded CAS PDF file + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - pdf_url: URL to the CAS PDF file + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) extra_headers: Send extra headers @@ -125,9 +125,9 @@ def cdsl( Args: password: Password for the PDF file (if required) - pdf_file: Base64 encoded CAS PDF file + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - pdf_url: URL to the CAS PDF file + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) extra_headers: Send extra headers @@ -181,9 +181,9 @@ def nsdl( Args: password: Password for the PDF file (if required) - pdf_file: Base64 encoded CAS PDF file + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - pdf_url: URL to the CAS PDF file + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) extra_headers: Send extra headers @@ -238,9 +238,9 @@ def smart_parse( Args: password: Password for the PDF file (if required) - pdf_file: Base64 encoded CAS PDF file + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - pdf_url: URL to the CAS PDF file + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) extra_headers: Send extra headers @@ -315,9 +315,9 @@ async def cams_kfintech( Args: password: Password for the PDF file (if required) - pdf_file: Base64 encoded CAS PDF file + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - pdf_url: URL to the CAS PDF file + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) extra_headers: Send extra headers @@ -371,9 +371,9 @@ async def cdsl( Args: password: Password for the PDF file (if required) - pdf_file: Base64 encoded CAS PDF file + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - pdf_url: URL to the CAS PDF file + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) extra_headers: Send extra headers @@ -427,9 +427,9 @@ async def nsdl( Args: password: Password for the PDF file (if required) - pdf_file: Base64 encoded CAS PDF file + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - pdf_url: URL to the CAS PDF file + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) extra_headers: Send extra headers @@ -484,9 +484,9 @@ async def smart_parse( Args: password: Password for the PDF file (if required) - pdf_file: Base64 encoded CAS PDF file + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - pdf_url: URL to the CAS PDF file + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) extra_headers: Send extra headers diff --git a/src/cas_parser/types/cas_parser_cams_kfintech_params.py b/src/cas_parser/types/cas_parser_cams_kfintech_params.py index 69b8597..f0a1552 100644 --- a/src/cas_parser/types/cas_parser_cams_kfintech_params.py +++ b/src/cas_parser/types/cas_parser_cams_kfintech_params.py @@ -12,7 +12,7 @@ class CasParserCamsKfintechParams(TypedDict, total=False): """Password for the PDF file (if required)""" pdf_file: str - """Base64 encoded CAS PDF file""" + """Base64 encoded CAS PDF file (required if pdf_url not provided)""" pdf_url: str - """URL to the CAS PDF file""" + """URL to the CAS PDF file (required if pdf_file not provided)""" diff --git a/src/cas_parser/types/cas_parser_cdsl_params.py b/src/cas_parser/types/cas_parser_cdsl_params.py index a25130c..725dc6d 100644 --- a/src/cas_parser/types/cas_parser_cdsl_params.py +++ b/src/cas_parser/types/cas_parser_cdsl_params.py @@ -12,7 +12,7 @@ class CasParserCdslParams(TypedDict, total=False): """Password for the PDF file (if required)""" pdf_file: str - """Base64 encoded CAS PDF file""" + """Base64 encoded CAS PDF file (required if pdf_url not provided)""" pdf_url: str - """URL to the CAS PDF file""" + """URL to the CAS PDF file (required if pdf_file not provided)""" diff --git a/src/cas_parser/types/cas_parser_nsdl_params.py b/src/cas_parser/types/cas_parser_nsdl_params.py index 51177cb..357f533 100644 --- a/src/cas_parser/types/cas_parser_nsdl_params.py +++ b/src/cas_parser/types/cas_parser_nsdl_params.py @@ -12,7 +12,7 @@ class CasParserNsdlParams(TypedDict, total=False): """Password for the PDF file (if required)""" pdf_file: str - """Base64 encoded CAS PDF file""" + """Base64 encoded CAS PDF file (required if pdf_url not provided)""" pdf_url: str - """URL to the CAS PDF file""" + """URL to the CAS PDF file (required if pdf_file not provided)""" diff --git a/src/cas_parser/types/cas_parser_smart_parse_params.py b/src/cas_parser/types/cas_parser_smart_parse_params.py index 003e28d..56af64f 100644 --- a/src/cas_parser/types/cas_parser_smart_parse_params.py +++ b/src/cas_parser/types/cas_parser_smart_parse_params.py @@ -12,7 +12,7 @@ class CasParserSmartParseParams(TypedDict, total=False): """Password for the PDF file (if required)""" pdf_file: str - """Base64 encoded CAS PDF file""" + """Base64 encoded CAS PDF file (required if pdf_url not provided)""" pdf_url: str - """URL to the CAS PDF file""" + """URL to the CAS PDF file (required if pdf_file not provided)""" From 10d4e86f18d44e9a9227fb18a66795a89bddbd37 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:19:20 +0000 Subject: [PATCH 041/116] chore: update SDK settings --- pyproject.toml | 2 +- requirements-dev.lock | 20 ++++++++++---------- requirements.lock | 8 ++++---- src/cas_parser/_utils/_compat.py | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f286b5e..85cf553 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ format = { chain = [ # run formatting again to fix any inconsistencies when imports are stripped "format:ruff", ]} -"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" +"format:docs" = "bash -c 'python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md)'" "format:ruff" = "ruff format" "lint" = { chain = [ diff --git a/requirements-dev.lock b/requirements-dev.lock index 1a3f9c1..4697e5f 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,14 +12,14 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via cas-parser-python # via httpx-aiohttp aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via cas-parser-python # via httpx argcomplete==3.6.3 @@ -31,7 +31,7 @@ attrs==25.4.0 # via nox backports-asyncio-runner==1.2.0 # via pytest-asyncio -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx colorlog==6.10.1 @@ -61,7 +61,7 @@ httpx==0.28.1 # via cas-parser-python # via httpx-aiohttp # via respx -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via cas-parser-python humanize==4.13.0 # via nox @@ -69,7 +69,7 @@ idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==8.7.0 +importlib-metadata==8.7.1 iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 @@ -82,14 +82,14 @@ multidict==6.7.0 mypy==1.17.0 mypy-extensions==1.1.0 # via mypy -nodeenv==1.9.1 +nodeenv==1.10.0 # via pyright nox==2025.11.12 packaging==25.0 # via dependency-groups # via nox # via pytest -pathspec==0.12.1 +pathspec==1.0.3 # via mypy platformdirs==4.4.0 # via virtualenv @@ -115,13 +115,13 @@ python-dateutil==2.9.0.post0 # via time-machine respx==0.22.0 rich==14.2.0 -ruff==0.14.7 +ruff==0.14.13 six==1.17.0 # via python-dateutil sniffio==1.3.1 # via cas-parser-python time-machine==2.19.0 -tomli==2.3.0 +tomli==2.4.0 # via dependency-groups # via mypy # via nox @@ -141,7 +141,7 @@ typing-extensions==4.15.0 # via virtualenv typing-inspection==0.4.2 # via pydantic -virtualenv==20.35.4 +virtualenv==20.36.1 # via nox yarl==1.22.0 # via aiohttp diff --git a/requirements.lock b/requirements.lock index 4fdd1ca..1ce37bc 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,21 +12,21 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via cas-parser-python # via httpx-aiohttp aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via cas-parser-python # via httpx async-timeout==5.0.1 # via aiohttp attrs==25.4.0 # via aiohttp -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx distro==1.9.0 @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via cas-parser-python # via httpx-aiohttp -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via cas-parser-python idna==3.11 # via anyio diff --git a/src/cas_parser/_utils/_compat.py b/src/cas_parser/_utils/_compat.py index dd70323..2c70b29 100644 --- a/src/cas_parser/_utils/_compat.py +++ b/src/cas_parser/_utils/_compat.py @@ -26,7 +26,7 @@ def is_union(tp: Optional[Type[Any]]) -> bool: else: import types - return tp is Union or tp is types.UnionType + return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap] def is_typeddict(tp: Type[Any]) -> bool: From 8d49800429cf25a3ab17f2d09ee1ed6fc4da428d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:20:41 +0000 Subject: [PATCH 042/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2601677..d0ab664 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.1.0" + ".": "1.2.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 85cf553..b181c80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.1.0" +version = "1.2.0" description = "The official Python library for the CAS Parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 69821a2..6ae1318 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.1.0" # x-release-please-version +__version__ = "1.2.0" # x-release-please-version From c42c367706b70d49b934d0b6c72102d07eea021d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:24:31 +0000 Subject: [PATCH 043/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d0ab664..d43a621 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.2.0" + ".": "1.2.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b181c80..b2e1d0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.2.0" +version = "1.2.1" description = "The official Python library for the CAS Parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 6ae1318..92a9acc 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.2.0" # x-release-please-version +__version__ = "1.2.1" # x-release-please-version From 30475e34cd9a1aacb2e5afe2aee8dcfadf49b998 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:29:50 +0000 Subject: [PATCH 044/116] feat(api): manual updates --- .devcontainer/Dockerfile | 3 +- .devcontainer/devcontainer.json | 4 +- .github/workflows/ci.yml | 39 +- .github/workflows/publish-pypi.yml | 31 - .github/workflows/release-doctor.yml | 21 - .release-please-manifest.json | 3 - .stats.yml | 4 +- Brewfile | 2 +- CONTRIBUTING.md | 26 +- README.md | 84 +- api.md | 133 +- bin/check-release-environment | 21 - bin/publish-pypi | 5 +- noxfile.py | 9 - pyproject.toml | 62 +- release-please-config.json | 66 - requirements-dev.lock | 163 +- requirements.lock | 76 - scripts/bootstrap | 11 +- scripts/format | 10 +- scripts/lint | 16 +- scripts/test | 31 +- src/cas_parser/__init__.py | 2 + src/cas_parser/_client.py | 505 ++++- src/cas_parser/_version.py | 2 +- src/cas_parser/resources/__init__.py | 166 +- src/cas_parser/resources/access_token.py | 191 ++ src/cas_parser/resources/cams_kfintech.py | 213 ++ src/cas_parser/resources/cas_parser.py | 592 ------ src/cas_parser/resources/cdsl/__init__.py | 33 + src/cas_parser/resources/cdsl/cdsl.py | 245 +++ src/cas_parser/resources/cdsl/fetch.py | 322 +++ src/cas_parser/resources/contract_note.py | 274 +++ src/cas_parser/resources/credits.py | 155 ++ src/cas_parser/resources/inbox.py | 534 +++++ src/cas_parser/resources/kfintech.py | 219 ++ src/cas_parser/resources/logs.py | 307 +++ src/cas_parser/resources/nsdl.py | 213 ++ src/cas_parser/resources/smart.py | 215 ++ src/cas_parser/resources/verify_token.py | 143 ++ src/cas_parser/types/__init__.py | 30 +- .../types/access_token_create_params.py | 12 + .../types/access_token_create_response.py | 18 + ...arams.py => cams_kfintech_parse_params.py} | 4 +- src/cas_parser/types/cdsl/__init__.py | 8 + .../types/cdsl/fetch_request_otp_params.py | 18 + .../types/cdsl/fetch_request_otp_response.py | 16 + .../types/cdsl/fetch_verify_otp_params.py | 15 + .../types/cdsl/fetch_verify_otp_response.py | 22 + ...dsl_params.py => cdsl_parse_pdf_params.py} | 4 +- .../types/contract_note_parse_params.py | 21 + .../types/contract_note_parse_response.py | 222 ++ src/cas_parser/types/credit_check_response.py | 28 + .../inbox_check_connection_status_response.py | 19 + .../types/inbox_connect_email_params.py | 15 + .../types/inbox_connect_email_response.py | 17 + .../types/inbox_disconnect_email_response.py | 13 + .../types/inbox_list_cas_files_params.py | 30 + .../types/inbox_list_cas_files_response.py | 46 + .../types/kfintech_generate_cas_params.py | 24 + .../types/kfintech_generate_cas_response.py | 13 + src/cas_parser/types/linked_holder.py | 15 + src/cas_parser/types/log_create_params.py | 22 + src/cas_parser/types/log_create_response.py | 37 + .../types/log_get_summary_params.py | 19 + .../types/log_get_summary_response.py | 35 + ...er_nsdl_params.py => nsdl_parse_params.py} | 4 +- ...arams.py => smart_parse_cas_pdf_params.py} | 4 +- src/cas_parser/types/transaction.py | 92 + src/cas_parser/types/unified_response.py | 565 +---- .../types/verify_token_verify_response.py | 18 + tests/api_resources/cdsl/__init__.py | 1 + tests/api_resources/cdsl/test_fetch.py | 219 ++ tests/api_resources/test_access_token.py | 96 + tests/api_resources/test_cams_kfintech.py | 100 + tests/api_resources/test_cas_parser.py | 330 --- tests/api_resources/test_cdsl.py | 100 + tests/api_resources/test_contract_note.py | 102 + tests/api_resources/test_credits.py | 80 + tests/api_resources/test_inbox.py | 342 +++ tests/api_resources/test_kfintech.py | 134 ++ tests/api_resources/test_logs.py | 175 ++ tests/api_resources/test_nsdl.py | 100 + tests/api_resources/test_smart.py | 100 + tests/api_resources/test_verify_token.py | 80 + tests/test_client.py | 68 +- uv.lock | 1829 +++++++++++++++++ 87 files changed, 8371 insertions(+), 2042 deletions(-) delete mode 100644 .github/workflows/publish-pypi.yml delete mode 100644 .github/workflows/release-doctor.yml delete mode 100644 .release-please-manifest.json delete mode 100644 bin/check-release-environment delete mode 100644 noxfile.py delete mode 100644 release-please-config.json delete mode 100644 requirements.lock create mode 100644 src/cas_parser/resources/access_token.py create mode 100644 src/cas_parser/resources/cams_kfintech.py delete mode 100644 src/cas_parser/resources/cas_parser.py create mode 100644 src/cas_parser/resources/cdsl/__init__.py create mode 100644 src/cas_parser/resources/cdsl/cdsl.py create mode 100644 src/cas_parser/resources/cdsl/fetch.py create mode 100644 src/cas_parser/resources/contract_note.py create mode 100644 src/cas_parser/resources/credits.py create mode 100644 src/cas_parser/resources/inbox.py create mode 100644 src/cas_parser/resources/kfintech.py create mode 100644 src/cas_parser/resources/logs.py create mode 100644 src/cas_parser/resources/nsdl.py create mode 100644 src/cas_parser/resources/smart.py create mode 100644 src/cas_parser/resources/verify_token.py create mode 100644 src/cas_parser/types/access_token_create_params.py create mode 100644 src/cas_parser/types/access_token_create_response.py rename src/cas_parser/types/{cas_parser_smart_parse_params.py => cams_kfintech_parse_params.py} (81%) create mode 100644 src/cas_parser/types/cdsl/__init__.py create mode 100644 src/cas_parser/types/cdsl/fetch_request_otp_params.py create mode 100644 src/cas_parser/types/cdsl/fetch_request_otp_response.py create mode 100644 src/cas_parser/types/cdsl/fetch_verify_otp_params.py create mode 100644 src/cas_parser/types/cdsl/fetch_verify_otp_response.py rename src/cas_parser/types/{cas_parser_cdsl_params.py => cdsl_parse_pdf_params.py} (82%) create mode 100644 src/cas_parser/types/contract_note_parse_params.py create mode 100644 src/cas_parser/types/contract_note_parse_response.py create mode 100644 src/cas_parser/types/credit_check_response.py create mode 100644 src/cas_parser/types/inbox_check_connection_status_response.py create mode 100644 src/cas_parser/types/inbox_connect_email_params.py create mode 100644 src/cas_parser/types/inbox_connect_email_response.py create mode 100644 src/cas_parser/types/inbox_disconnect_email_response.py create mode 100644 src/cas_parser/types/inbox_list_cas_files_params.py create mode 100644 src/cas_parser/types/inbox_list_cas_files_response.py create mode 100644 src/cas_parser/types/kfintech_generate_cas_params.py create mode 100644 src/cas_parser/types/kfintech_generate_cas_response.py create mode 100644 src/cas_parser/types/linked_holder.py create mode 100644 src/cas_parser/types/log_create_params.py create mode 100644 src/cas_parser/types/log_create_response.py create mode 100644 src/cas_parser/types/log_get_summary_params.py create mode 100644 src/cas_parser/types/log_get_summary_response.py rename src/cas_parser/types/{cas_parser_nsdl_params.py => nsdl_parse_params.py} (82%) rename src/cas_parser/types/{cas_parser_cams_kfintech_params.py => smart_parse_cas_pdf_params.py} (80%) create mode 100644 src/cas_parser/types/transaction.py create mode 100644 src/cas_parser/types/verify_token_verify_response.py create mode 100644 tests/api_resources/cdsl/__init__.py create mode 100644 tests/api_resources/cdsl/test_fetch.py create mode 100644 tests/api_resources/test_access_token.py create mode 100644 tests/api_resources/test_cams_kfintech.py delete mode 100644 tests/api_resources/test_cas_parser.py create mode 100644 tests/api_resources/test_cdsl.py create mode 100644 tests/api_resources/test_contract_note.py create mode 100644 tests/api_resources/test_credits.py create mode 100644 tests/api_resources/test_inbox.py create mode 100644 tests/api_resources/test_kfintech.py create mode 100644 tests/api_resources/test_logs.py create mode 100644 tests/api_resources/test_nsdl.py create mode 100644 tests/api_resources/test_smart.py create mode 100644 tests/api_resources/test_verify_token.py create mode 100644 uv.lock diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ff261ba..62c2d13 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,6 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} USER vscode -RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash -ENV PATH=/home/vscode/.rye/shims:$PATH +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c17fdc1..e01283d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,7 +7,7 @@ "context": ".." }, - "postStartCommand": "rye sync --all-features", + "postStartCommand": "uv sync --all-extras", "customizations": { "vscode": { @@ -20,7 +20,7 @@ "python.defaultInterpreterPath": ".venv/bin/python", "python.typeChecking": "basic", "terminal.integrated.env.linux": { - "PATH": "/home/vscode/.rye/shims:${env:PATH}" + "PATH": "${env:PATH}" } } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06d6959..bbbfbc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,16 +21,13 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.9.13' - name: Install dependencies - run: rye sync --all-features + run: uv sync --all-extras - name: Run lints run: ./scripts/lint @@ -46,19 +43,16 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.9.13' - name: Install dependencies - run: rye sync --all-features + run: uv sync --all-extras - name: Run build - run: rye build + run: uv build - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/cas-parser-python' @@ -83,13 +77,10 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.9.13' - name: Bootstrap run: ./scripts/bootstrap diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml deleted file mode 100644 index 9a3087b..0000000 --- a/.github/workflows/publish-pypi.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This workflow is triggered when a GitHub release is created. -# It can also be run manually to re-publish to PyPI in case it failed for some reason. -# You can run this workflow by navigating to https://www.github.com/CASParser/cas-parser-python/actions/workflows/publish-pypi.yml -name: Publish PyPI -on: - workflow_dispatch: - - release: - types: [published] - -jobs: - publish: - name: publish - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' - - - name: Publish to PyPI - run: | - bash ./bin/publish-pypi - env: - PYPI_TOKEN: ${{ secrets.CAS_PARSER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml deleted file mode 100644 index a77924a..0000000 --- a/.github/workflows/release-doctor.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Release Doctor -on: - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - release_doctor: - name: release doctor - runs-on: ubuntu-latest - if: github.repository == 'CASParser/cas-parser-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') - - steps: - - uses: actions/checkout@v6 - - - name: Check release environment - run: | - bash ./bin/check-release-environment - env: - PYPI_TOKEN: ${{ secrets.CAS_PARSER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json deleted file mode 100644 index d43a621..0000000 --- a/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "1.2.1" -} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 2c90fa6..ff35822 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 4 +configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: cb5d75abef6264b5d86448caf7295afa +config_hash: 267be54ae6400ea329b16189da51ee06 diff --git a/Brewfile b/Brewfile index 492ca37..c43041c 100644 --- a/Brewfile +++ b/Brewfile @@ -1,2 +1,2 @@ -brew "rye" +brew "uv" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9de374b..13c0870 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,32 +1,32 @@ ## Setting up the environment -### With Rye +### With `uv` -We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: +We use [uv](https://docs.astral.sh/uv/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: ```sh $ ./scripts/bootstrap ``` -Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: +Or [install uv manually](https://docs.astral.sh/uv/getting-started/installation/) and run: ```sh -$ rye sync --all-features +$ uv sync --all-extras ``` -You can then run scripts using `rye run python script.py` or by activating the virtual environment: +You can then run scripts using `uv run python script.py` or by manually activating the virtual environment: ```sh -# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work +# manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work $ source .venv/bin/activate -# now you can omit the `rye run` prefix +# now you can omit the `uv run` prefix $ python script.py ``` -### Without Rye +### Without `uv` -Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: +Alternatively if you don't want to install `uv`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: ```sh $ pip install -r requirements-dev.lock @@ -45,7 +45,7 @@ All files in the `examples/` directory are not modified by the generator and can ```py # add an example to examples/.py -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S uv run python … ``` @@ -62,7 +62,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```sh -$ pip install git+ssh://git@github.com/CASParser/cas-parser-python.git +$ pip install git+ssh://git@github.com/stainless-sdks/cas-parser-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -72,7 +72,7 @@ Building this package will create two files in the `dist/` directory, a `.tar.gz To create a distributable version of the library, all you have to do is run this command: ```sh -$ rye build +$ uv build # or $ python -m build ``` @@ -120,7 +120,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/CASParser/cas-parser-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/cas-parser-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index 6ef2f56..21886e4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Cas Parser Python API library -[![PyPI version](https://img.shields.io/pypi/v/cas-parser-python.svg?label=pypi%20(stable))](https://pypi.org/project/cas-parser-python/) +[![PyPI version](https://img.shields.io/pypi/v/cas_parser.svg?label=pypi%20(stable))](https://pypi.org/project/cas_parser/) The Cas Parser Python library provides convenient access to the Cas Parser REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, @@ -9,26 +9,20 @@ and offers both synchronous and asynchronous clients powered by [httpx](https:// It is generated with [Stainless](https://www.stainless.com/). -## MCP Server - -Use the Cas Parser MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. - -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=cas-parser-node-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNhcy1wYXJzZXItbm9kZS1tY3AiXSwiZW52Ijp7IkNBU19QQVJTRVJfQVBJX0tFWSI6Ik15IEFQSSBLZXkifX0) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-node-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22cas-parser-node-mcp%22%5D%2C%22env%22%3A%7B%22CAS_PARSER_API_KEY%22%3A%22My%20API%20Key%22%7D%7D) - -> Note: You may need to set environment variables in your MCP client. - ## Documentation -The REST API documentation can be found on [docs.casparser.in](https://docs.casparser.in/reference). The full API of this library can be found in [api.md](api.md). +The full API of this library can be found in [api.md](api.md). ## Installation ```sh -# install from PyPI -pip install cas-parser-python +# install from this staging repo +pip install git+ssh://git@github.com/stainless-sdks/cas-parser-python.git ``` +> [!NOTE] +> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install cas_parser` + ## Usage The full API of this library can be found in [api.md](api.md). @@ -39,13 +33,12 @@ from cas_parser import CasParser client = CasParser( api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted + # or 'production' | 'environment_2'; defaults to "production". + environment="environment_1", ) -unified_response = client.cas_parser.smart_parse( - password="ABCDF", - pdf_url="https://your-cas-pdf-url-here.com", -) -print(unified_response.demat_accounts) +response = client.credits.check() +print(response.enabled_features) ``` While you can provide an `api_key` keyword argument, @@ -64,15 +57,14 @@ from cas_parser import AsyncCasParser client = AsyncCasParser( api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted + # or 'production' | 'environment_2'; defaults to "production". + environment="environment_1", ) async def main() -> None: - unified_response = await client.cas_parser.smart_parse( - password="ABCDF", - pdf_url="https://your-cas-pdf-url-here.com", - ) - print(unified_response.demat_accounts) + response = await client.credits.check() + print(response.enabled_features) asyncio.run(main()) @@ -87,8 +79,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from PyPI -pip install cas-parser-python[aiohttp] +# install from this staging repo +pip install 'cas_parser[aiohttp] @ git+ssh://git@github.com/stainless-sdks/cas-parser-python.git' ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: @@ -105,11 +97,8 @@ async def main() -> None: api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: - unified_response = await client.cas_parser.smart_parse( - password="ABCDF", - pdf_url="https://your-cas-pdf-url-here.com", - ) - print(unified_response.demat_accounts) + response = await client.credits.check() + print(response.enabled_features) asyncio.run(main()) @@ -140,10 +129,7 @@ from cas_parser import CasParser client = CasParser() try: - client.cas_parser.smart_parse( - password="ABCDF", - pdf_url="https://you-cas-pdf-url-here.com", - ) + client.credits.check() except cas_parser.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. @@ -186,10 +172,7 @@ client = CasParser( ) # Or, configure per-request: -client.with_options(max_retries=5).cas_parser.smart_parse( - password="ABCDF", - pdf_url="https://you-cas-pdf-url-here.com", -) +client.with_options(max_retries=5).credits.check() ``` ### Timeouts @@ -212,10 +195,7 @@ client = CasParser( ) # Override per-request: -client.with_options(timeout=5.0).cas_parser.smart_parse( - password="ABCDF", - pdf_url="https://you-cas-pdf-url-here.com", -) +client.with_options(timeout=5.0).credits.check() ``` On timeout, an `APITimeoutError` is thrown. @@ -256,19 +236,16 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from cas_parser import CasParser client = CasParser() -response = client.cas_parser.with_raw_response.smart_parse( - password="ABCDF", - pdf_url="https://you-cas-pdf-url-here.com", -) +response = client.credits.with_raw_response.check() print(response.headers.get('X-My-Header')) -cas_parser = response.parse() # get the object that `cas_parser.smart_parse()` would have returned -print(cas_parser.demat_accounts) +credit = response.parse() # get the object that `credits.check()` would have returned +print(credit.enabled_features) ``` -These methods return an [`APIResponse`](https://github.com/CASParser/cas-parser-python/tree/main/src/cas_parser/_response.py) object. +These methods return an [`APIResponse`](https://github.com/stainless-sdks/cas-parser-python/tree/main/src/cas_parser/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/CASParser/cas-parser-python/tree/main/src/cas_parser/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/cas-parser-python/tree/main/src/cas_parser/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -277,10 +254,7 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.cas_parser.with_streaming_response.smart_parse( - password="ABCDF", - pdf_url="https://you-cas-pdf-url-here.com", -) as response: +with client.credits.with_streaming_response.check() as response: print(response.headers.get("X-My-Header")) for line in response.iter_lines(): @@ -375,7 +349,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/CASParser/cas-parser-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/cas-parser-python/issues) with questions, bugs, or suggestions. ### Determining the installed version diff --git a/api.md b/api.md index 7a55253..d92590b 100644 --- a/api.md +++ b/api.md @@ -1,14 +1,135 @@ -# CasParser +# Credits Types: ```python -from cas_parser.types import UnifiedResponse +from cas_parser.types import CreditCheckResponse ``` Methods: -- client.cas_parser.cams_kfintech(\*\*params) -> UnifiedResponse -- client.cas_parser.cdsl(\*\*params) -> UnifiedResponse -- client.cas_parser.nsdl(\*\*params) -> UnifiedResponse -- client.cas_parser.smart_parse(\*\*params) -> UnifiedResponse +- client.credits.check() -> CreditCheckResponse + +# Logs + +Types: + +```python +from cas_parser.types import LogCreateResponse, LogGetSummaryResponse +``` + +Methods: + +- client.logs.create(\*\*params) -> LogCreateResponse +- client.logs.get_summary(\*\*params) -> LogGetSummaryResponse + +# AccessToken + +Types: + +```python +from cas_parser.types import AccessTokenCreateResponse +``` + +Methods: + +- client.access_token.create(\*\*params) -> AccessTokenCreateResponse + +# VerifyToken + +Types: + +```python +from cas_parser.types import VerifyTokenVerifyResponse +``` + +Methods: + +- client.verify_token.verify() -> VerifyTokenVerifyResponse + +# CamsKfintech + +Types: + +```python +from cas_parser.types import LinkedHolder, Transaction, UnifiedResponse +``` + +Methods: + +- client.cams_kfintech.parse(\*\*params) -> UnifiedResponse + +# Cdsl + +Methods: + +- client.cdsl.parse_pdf(\*\*params) -> UnifiedResponse + +## Fetch + +Types: + +```python +from cas_parser.types.cdsl import FetchRequestOtpResponse, FetchVerifyOtpResponse +``` + +Methods: + +- client.cdsl.fetch.request_otp(\*\*params) -> FetchRequestOtpResponse +- client.cdsl.fetch.verify_otp(session_id, \*\*params) -> FetchVerifyOtpResponse + +# ContractNote + +Types: + +```python +from cas_parser.types import ContractNoteParseResponse +``` + +Methods: + +- client.contract_note.parse(\*\*params) -> ContractNoteParseResponse + +# Inbox + +Types: + +```python +from cas_parser.types import ( + InboxCheckConnectionStatusResponse, + InboxConnectEmailResponse, + InboxDisconnectEmailResponse, + InboxListCasFilesResponse, +) +``` + +Methods: + +- client.inbox.check_connection_status() -> InboxCheckConnectionStatusResponse +- client.inbox.connect_email(\*\*params) -> InboxConnectEmailResponse +- client.inbox.disconnect_email() -> InboxDisconnectEmailResponse +- client.inbox.list_cas_files(\*\*params) -> InboxListCasFilesResponse + +# Kfintech + +Types: + +```python +from cas_parser.types import KfintechGenerateCasResponse +``` + +Methods: + +- client.kfintech.generate_cas(\*\*params) -> KfintechGenerateCasResponse + +# Nsdl + +Methods: + +- client.nsdl.parse(\*\*params) -> UnifiedResponse + +# Smart + +Methods: + +- client.smart.parse_cas_pdf(\*\*params) -> UnifiedResponse diff --git a/bin/check-release-environment b/bin/check-release-environment deleted file mode 100644 index b845b0f..0000000 --- a/bin/check-release-environment +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -errors=() - -if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") -fi - -lenErrors=${#errors[@]} - -if [[ lenErrors -gt 0 ]]; then - echo -e "Found the following errors in the release environment:\n" - - for error in "${errors[@]}"; do - echo -e "- $error\n" - done - - exit 1 -fi - -echo "The environment is ready to push releases!" diff --git a/bin/publish-pypi b/bin/publish-pypi index 826054e..e72ca2f 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -eux +rm -rf dist mkdir -p dist -rye build --clean -rye publish --yes --token=$PYPI_TOKEN +uv build +uv publish --token=$PYPI_TOKEN diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index 53bca7f..0000000 --- a/noxfile.py +++ /dev/null @@ -1,9 +0,0 @@ -import nox - - -@nox.session(reuse_venv=True, name="test-pydantic-v1") -def test_pydantic_v1(session: nox.Session) -> None: - session.install("-r", "requirements-dev.lock") - session.install("pydantic<2") - - session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) diff --git a/pyproject.toml b/pyproject.toml index b2e1d0b..fe032ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "cas-parser-python" +name = "cas_parser" version = "1.2.1" -description = "The official Python library for the CAS Parser API" +description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" authors = [ @@ -37,16 +37,25 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/CASParser/cas-parser-python" -Repository = "https://github.com/CASParser/cas-parser-python" +Homepage = "https://github.com/stainless-sdks/cas-parser-python" +Repository = "https://github.com/stainless-sdks/cas-parser-python" [project.optional-dependencies] aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] -[tool.rye] +[tool.uv] managed = true -# version pins are in requirements-dev.lock -dev-dependencies = [ +required-version = ">=0.9" +conflicts = [ + [ + { group = "pydantic-v1" }, + { group = "pydantic-v2" }, + ], +] + +[dependency-groups] +# version pins are in uv.lock +dev = [ "pyright==1.1.399", "mypy==1.17", "respx", @@ -54,41 +63,18 @@ dev-dependencies = [ "pytest-asyncio", "ruff", "time-machine", - "nox", "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", "pytest-xdist>=3.6.1", ] - -[tool.rye.scripts] -format = { chain = [ - "format:ruff", - "format:docs", - "fix:ruff", - # run formatting again to fix any inconsistencies when imports are stripped - "format:ruff", -]} -"format:docs" = "bash -c 'python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md)'" -"format:ruff" = "ruff format" - -"lint" = { chain = [ - "check:ruff", - "typecheck", - "check:importable", -]} -"check:ruff" = "ruff check ." -"fix:ruff" = "ruff check --fix ." - -"check:importable" = "python -c 'import cas_parser'" - -typecheck = { chain = [ - "typecheck:pyright", - "typecheck:mypy" -]} -"typecheck:pyright" = "pyright" -"typecheck:verify-types" = "pyright --verifytypes cas_parser --ignoreexternal" -"typecheck:mypy" = "mypy ." +pydantic-v1 = [ + "pydantic>=1.9.0,<2", +] +pydantic-v2 = [ + "pydantic~=2.0 ; python_full_version < '3.14'", + "pydantic~=2.12 ; python_full_version >= '3.14'", +] [build-system] requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] @@ -126,7 +112,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/CASParser/cas-parser-python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/stainless-sdks/cas-parser-python/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/release-please-config.json b/release-please-config.json deleted file mode 100644 index 316db21..0000000 --- a/release-please-config.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "packages": { - ".": {} - }, - "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", - "include-v-in-tag": true, - "include-component-in-tag": false, - "versioning": "prerelease", - "prerelease": true, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": false, - "pull-request-header": "Automated Release PR", - "pull-request-title-pattern": "release: ${version}", - "changelog-sections": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "perf", - "section": "Performance Improvements" - }, - { - "type": "revert", - "section": "Reverts" - }, - { - "type": "chore", - "section": "Chores" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "style", - "section": "Styles" - }, - { - "type": "refactor", - "section": "Refactors" - }, - { - "type": "test", - "section": "Tests", - "hidden": true - }, - { - "type": "build", - "section": "Build System" - }, - { - "type": "ci", - "section": "Continuous Integration", - "hidden": true - } - ], - "release-type": "python", - "extra-files": [ - "src/cas_parser/_version.py" - ] -} \ No newline at end of file diff --git a/requirements-dev.lock b/requirements-dev.lock index 4697e5f..9ca2ac3 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -1,149 +1,110 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.13.3 - # via cas-parser-python - # via httpx-aiohttp -aiosignal==1.4.0 - # via aiohttp +# This file was autogenerated by uv via the following command: +# uv export -o requirements-dev.lock --no-hashes +-e . annotated-types==0.7.0 # via pydantic anyio==4.12.1 - # via cas-parser-python - # via httpx -argcomplete==3.6.3 - # via nox -async-timeout==5.0.1 - # via aiohttp -attrs==25.4.0 - # via aiohttp - # via nox -backports-asyncio-runner==1.2.0 + # via + # cas-parser + # httpx +backports-asyncio-runner==1.2.0 ; python_full_version < '3.11' # via pytest-asyncio certifi==2026.1.4 - # via httpcore - # via httpx -colorlog==6.10.1 - # via nox -dependency-groups==1.3.1 - # via nox + # via + # httpcore + # httpx +colorama==0.4.6 ; sys_platform == 'win32' + # via pytest dirty-equals==0.11 -distlib==0.4.0 - # via virtualenv distro==1.9.0 - # via cas-parser-python -exceptiongroup==1.3.1 - # via anyio - # via pytest + # via cas-parser +exceptiongroup==1.3.1 ; python_full_version < '3.11' + # via + # anyio + # pytest execnet==2.1.2 # via pytest-xdist -filelock==3.19.1 - # via virtualenv -frozenlist==1.8.0 - # via aiohttp - # via aiosignal h11==0.16.0 # via httpcore httpcore==1.0.9 # via httpx httpx==0.28.1 - # via cas-parser-python - # via httpx-aiohttp - # via respx -httpx-aiohttp==0.1.12 - # via cas-parser-python -humanize==4.13.0 - # via nox + # via + # cas-parser + # respx idna==3.11 - # via anyio - # via httpx - # via yarl + # via + # anyio + # httpx importlib-metadata==8.7.1 -iniconfig==2.1.0 +iniconfig==2.1.0 ; python_full_version < '3.10' + # via pytest +iniconfig==2.3.0 ; python_full_version >= '3.10' # via pytest -markdown-it-py==3.0.0 +markdown-it-py==3.0.0 ; python_full_version < '3.10' + # via rich +markdown-it-py==4.0.0 ; python_full_version >= '3.10' # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.7.0 - # via aiohttp - # via yarl mypy==1.17.0 mypy-extensions==1.1.0 # via mypy nodeenv==1.10.0 # via pyright -nox==2025.11.12 packaging==25.0 - # via dependency-groups - # via nox # via pytest pathspec==1.0.3 # via mypy -platformdirs==4.4.0 - # via virtualenv pluggy==1.6.0 # via pytest -propcache==0.4.1 - # via aiohttp - # via yarl pydantic==2.12.5 - # via cas-parser-python + # via cas-parser pydantic-core==2.41.5 # via pydantic pygments==2.19.2 - # via pytest - # via rich + # via + # pytest + # rich pyright==1.1.399 -pytest==8.4.2 - # via pytest-asyncio - # via pytest-xdist -pytest-asyncio==1.2.0 +pytest==8.4.2 ; python_full_version < '3.10' + # via + # pytest-asyncio + # pytest-xdist +pytest==9.0.2 ; python_full_version >= '3.10' + # via + # pytest-asyncio + # pytest-xdist +pytest-asyncio==1.2.0 ; python_full_version < '3.10' +pytest-asyncio==1.3.0 ; python_full_version >= '3.10' pytest-xdist==3.8.0 -python-dateutil==2.9.0.post0 +python-dateutil==2.9.0.post0 ; python_full_version < '3.10' # via time-machine respx==0.22.0 rich==14.2.0 ruff==0.14.13 -six==1.17.0 +six==1.17.0 ; python_full_version < '3.10' # via python-dateutil sniffio==1.3.1 - # via cas-parser-python -time-machine==2.19.0 -tomli==2.4.0 - # via dependency-groups - # via mypy - # via nox - # via pytest + # via cas-parser +time-machine==2.19.0 ; python_full_version < '3.10' +time-machine==3.2.0 ; python_full_version >= '3.10' +tomli==2.4.0 ; python_full_version < '3.11' + # via + # mypy + # pytest typing-extensions==4.15.0 - # via aiosignal - # via anyio - # via cas-parser-python - # via exceptiongroup - # via multidict - # via mypy - # via pydantic - # via pydantic-core - # via pyright - # via pytest-asyncio - # via typing-inspection - # via virtualenv + # via + # anyio + # cas-parser + # exceptiongroup + # mypy + # pydantic + # pydantic-core + # pyright + # pytest-asyncio + # typing-inspection typing-inspection==0.4.2 # via pydantic -virtualenv==20.36.1 - # via nox -yarl==1.22.0 - # via aiohttp zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock deleted file mode 100644 index 1ce37bc..0000000 --- a/requirements.lock +++ /dev/null @@ -1,76 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.13.3 - # via cas-parser-python - # via httpx-aiohttp -aiosignal==1.4.0 - # via aiohttp -annotated-types==0.7.0 - # via pydantic -anyio==4.12.1 - # via cas-parser-python - # via httpx -async-timeout==5.0.1 - # via aiohttp -attrs==25.4.0 - # via aiohttp -certifi==2026.1.4 - # via httpcore - # via httpx -distro==1.9.0 - # via cas-parser-python -exceptiongroup==1.3.1 - # via anyio -frozenlist==1.8.0 - # via aiohttp - # via aiosignal -h11==0.16.0 - # via httpcore -httpcore==1.0.9 - # via httpx -httpx==0.28.1 - # via cas-parser-python - # via httpx-aiohttp -httpx-aiohttp==0.1.12 - # via cas-parser-python -idna==3.11 - # via anyio - # via httpx - # via yarl -multidict==6.7.0 - # via aiohttp - # via yarl -propcache==0.4.1 - # via aiohttp - # via yarl -pydantic==2.12.5 - # via cas-parser-python -pydantic-core==2.41.5 - # via pydantic -sniffio==1.3.1 - # via cas-parser-python -typing-extensions==4.15.0 - # via aiosignal - # via anyio - # via cas-parser-python - # via exceptiongroup - # via multidict - # via pydantic - # via pydantic-core - # via typing-inspection -typing-inspection==0.4.2 - # via pydantic -yarl==1.22.0 - # via aiohttp diff --git a/scripts/bootstrap b/scripts/bootstrap index b430fee..4638ec6 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -19,9 +19,12 @@ if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] } fi -echo "==> Installing Python dependencies…" +echo "==> Installing Python…" +uv python install -# experimental uv support makes installations significantly faster -rye config --set-bool behavior.use-uv=true +echo "==> Installing Python dependencies…" +uv sync --all-extras -rye sync --all-features +echo "==> Exporting Python dependencies…" +# note: `--no-hashes` is required because of https://github.com/pypa/pip/issues/4995 +uv export -o requirements-dev.lock --no-hashes diff --git a/scripts/format b/scripts/format index 667ec2d..c8e1f69 100755 --- a/scripts/format +++ b/scripts/format @@ -4,5 +4,11 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running formatters" -rye run format +echo "==> Running ruff" +uv run ruff format +uv run ruff check --fix . +# run formatting again to fix any inconsistencies when imports are stripped +uv run ruff format + +echo "==> Formatting docs" +uv run python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md) diff --git a/scripts/lint b/scripts/lint index e1bf7a7..60d29e8 100755 --- a/scripts/lint +++ b/scripts/lint @@ -5,12 +5,18 @@ set -e cd "$(dirname "$0")/.." if [ "$1" = "--fix" ]; then - echo "==> Running lints with --fix" - rye run fix:ruff + echo "==> Running ruff with --fix" + uv run ruff check . --fix else - echo "==> Running lints" - rye run lint + echo "==> Running ruff" + uv run ruff check . fi +echo "==> Running pyright" +uv run pyright + +echo "==> Running mypy" +uv run mypy . + echo "==> Making sure it imports" -rye run python -c 'import cas_parser' +uv run python -c 'import cas_parser' diff --git a/scripts/test b/scripts/test index dbeda2d..b56970b 100755 --- a/scripts/test +++ b/scripts/test @@ -54,8 +54,31 @@ fi export DEFER_PYDANTIC_BUILD=false -echo "==> Running tests" -rye run pytest "$@" +# Note that we need to specify the patch version here so that uv +# won't use unstable (alpha, beta, rc) releases for the tests +PY_VERSION_MIN=">=3.9.0" +PY_VERSION_MAX=">=3.14.0" -echo "==> Running Pydantic v1 tests" -rye run nox -s test-pydantic-v1 -- "$@" +function run_tests() { + echo "==> Running tests with Pydantic v2" + uv run --isolated --all-extras pytest "$@" + + # Skip Pydantic v1 tests on latest Python (not supported) + if [[ "$UV_PYTHON" != "$PY_VERSION_MAX" ]]; then + echo "==> Running tests with Pydantic v1" + uv run --isolated --all-extras --group=pydantic-v1 pytest "$@" + fi +} + +# If UV_PYTHON is already set in the environment, just run the command once +if [[ -n "$UV_PYTHON" ]]; then + run_tests "$@" +else + # If UV_PYTHON is not set, run the command for min and max versions + + echo "==> Running tests for Python $PY_VERSION_MIN" + UV_PYTHON="$PY_VERSION_MIN" run_tests "$@" + + echo "==> Running tests for Python $PY_VERSION_MAX" + UV_PYTHON="$PY_VERSION_MAX" run_tests "$@" +fi diff --git a/src/cas_parser/__init__.py b/src/cas_parser/__init__.py index 1e1d246..a146f39 100644 --- a/src/cas_parser/__init__.py +++ b/src/cas_parser/__init__.py @@ -6,6 +6,7 @@ from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given from ._utils import file_from_path from ._client import ( + ENVIRONMENTS, Client, Stream, Timeout, @@ -73,6 +74,7 @@ "AsyncStream", "CasParser", "AsyncCasParser", + "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py index b84a489..02dc18a 100644 --- a/src/cas_parser/_client.py +++ b/src/cas_parser/_client.py @@ -3,8 +3,8 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING, Any, Mapping -from typing_extensions import Self, override +from typing import TYPE_CHECKING, Any, Dict, Mapping, cast +from typing_extensions import Self, Literal, override import httpx @@ -31,10 +31,33 @@ ) if TYPE_CHECKING: - from .resources import cas_parser - from .resources.cas_parser import CasParserResource, AsyncCasParserResource + from .resources import ( + cdsl, + logs, + nsdl, + inbox, + smart, + credits, + kfintech, + access_token, + verify_token, + cams_kfintech, + contract_note, + ) + from .resources.logs import LogsResource, AsyncLogsResource + from .resources.nsdl import NsdlResource, AsyncNsdlResource + from .resources.inbox import InboxResource, AsyncInboxResource + from .resources.smart import SmartResource, AsyncSmartResource + from .resources.credits import CreditsResource, AsyncCreditsResource + from .resources.kfintech import KfintechResource, AsyncKfintechResource + from .resources.cdsl.cdsl import CdslResource, AsyncCdslResource + from .resources.access_token import AccessTokenResource, AsyncAccessTokenResource + from .resources.verify_token import VerifyTokenResource, AsyncVerifyTokenResource + from .resources.cams_kfintech import CamsKfintechResource, AsyncCamsKfintechResource + from .resources.contract_note import ContractNoteResource, AsyncContractNoteResource __all__ = [ + "ENVIRONMENTS", "Timeout", "Transport", "ProxiesTypes", @@ -45,16 +68,25 @@ "AsyncClient", ] +ENVIRONMENTS: Dict[str, str] = { + "production": "https://portfolio-parser.api.casparser.in", + "environment_1": "https://client-apis.casparser.in", + "environment_2": "http://localhost:5000", +} + class CasParser(SyncAPIClient): # client options api_key: str + _environment: Literal["production", "environment_1", "environment_2"] | NotGiven + def __init__( self, *, api_key: str | None = None, - base_url: str | httpx.URL | None = None, + environment: Literal["production", "environment_1", "environment_2"] | NotGiven = not_given, + base_url: str | httpx.URL | None | NotGiven = not_given, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -85,10 +117,31 @@ def __init__( ) self.api_key = api_key - if base_url is None: - base_url = os.environ.get("CAS_PARSER_BASE_URL") - if base_url is None: - base_url = f"https://portfolio-parser.api.casparser.in" + self._environment = environment + + base_url_env = os.environ.get("CAS_PARSER_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `CAS_PARSER_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc super().__init__( version=__version__, @@ -102,10 +155,70 @@ def __init__( ) @cached_property - def cas_parser(self) -> CasParserResource: - from .resources.cas_parser import CasParserResource + def credits(self) -> CreditsResource: + from .resources.credits import CreditsResource + + return CreditsResource(self) + + @cached_property + def logs(self) -> LogsResource: + from .resources.logs import LogsResource + + return LogsResource(self) + + @cached_property + def access_token(self) -> AccessTokenResource: + from .resources.access_token import AccessTokenResource + + return AccessTokenResource(self) + + @cached_property + def verify_token(self) -> VerifyTokenResource: + from .resources.verify_token import VerifyTokenResource + + return VerifyTokenResource(self) + + @cached_property + def cams_kfintech(self) -> CamsKfintechResource: + from .resources.cams_kfintech import CamsKfintechResource + + return CamsKfintechResource(self) + + @cached_property + def cdsl(self) -> CdslResource: + from .resources.cdsl import CdslResource + + return CdslResource(self) + + @cached_property + def contract_note(self) -> ContractNoteResource: + from .resources.contract_note import ContractNoteResource + + return ContractNoteResource(self) + + @cached_property + def inbox(self) -> InboxResource: + from .resources.inbox import InboxResource - return CasParserResource(self) + return InboxResource(self) + + @cached_property + def kfintech(self) -> KfintechResource: + from .resources.kfintech import KfintechResource + + return KfintechResource(self) + + @cached_property + def nsdl(self) -> NsdlResource: + from .resources.nsdl import NsdlResource + + return NsdlResource(self) + + @cached_property + def smart(self) -> SmartResource: + from .resources.smart import SmartResource + + return SmartResource(self) @cached_property def with_raw_response(self) -> CasParserWithRawResponse: @@ -139,6 +252,7 @@ def copy( self, *, api_key: str | None = None, + environment: Literal["production", "environment_1", "environment_2"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, @@ -174,6 +288,7 @@ def copy( return self.__class__( api_key=api_key or self.api_key, base_url=base_url or self.base_url, + environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, @@ -224,11 +339,14 @@ class AsyncCasParser(AsyncAPIClient): # client options api_key: str + _environment: Literal["production", "environment_1", "environment_2"] | NotGiven + def __init__( self, *, api_key: str | None = None, - base_url: str | httpx.URL | None = None, + environment: Literal["production", "environment_1", "environment_2"] | NotGiven = not_given, + base_url: str | httpx.URL | None | NotGiven = not_given, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -259,10 +377,31 @@ def __init__( ) self.api_key = api_key - if base_url is None: - base_url = os.environ.get("CAS_PARSER_BASE_URL") - if base_url is None: - base_url = f"https://portfolio-parser.api.casparser.in" + self._environment = environment + + base_url_env = os.environ.get("CAS_PARSER_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `CAS_PARSER_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc super().__init__( version=__version__, @@ -276,10 +415,70 @@ def __init__( ) @cached_property - def cas_parser(self) -> AsyncCasParserResource: - from .resources.cas_parser import AsyncCasParserResource + def credits(self) -> AsyncCreditsResource: + from .resources.credits import AsyncCreditsResource + + return AsyncCreditsResource(self) + + @cached_property + def logs(self) -> AsyncLogsResource: + from .resources.logs import AsyncLogsResource + + return AsyncLogsResource(self) + + @cached_property + def access_token(self) -> AsyncAccessTokenResource: + from .resources.access_token import AsyncAccessTokenResource + + return AsyncAccessTokenResource(self) + + @cached_property + def verify_token(self) -> AsyncVerifyTokenResource: + from .resources.verify_token import AsyncVerifyTokenResource + + return AsyncVerifyTokenResource(self) + + @cached_property + def cams_kfintech(self) -> AsyncCamsKfintechResource: + from .resources.cams_kfintech import AsyncCamsKfintechResource - return AsyncCasParserResource(self) + return AsyncCamsKfintechResource(self) + + @cached_property + def cdsl(self) -> AsyncCdslResource: + from .resources.cdsl import AsyncCdslResource + + return AsyncCdslResource(self) + + @cached_property + def contract_note(self) -> AsyncContractNoteResource: + from .resources.contract_note import AsyncContractNoteResource + + return AsyncContractNoteResource(self) + + @cached_property + def inbox(self) -> AsyncInboxResource: + from .resources.inbox import AsyncInboxResource + + return AsyncInboxResource(self) + + @cached_property + def kfintech(self) -> AsyncKfintechResource: + from .resources.kfintech import AsyncKfintechResource + + return AsyncKfintechResource(self) + + @cached_property + def nsdl(self) -> AsyncNsdlResource: + from .resources.nsdl import AsyncNsdlResource + + return AsyncNsdlResource(self) + + @cached_property + def smart(self) -> AsyncSmartResource: + from .resources.smart import AsyncSmartResource + + return AsyncSmartResource(self) @cached_property def with_raw_response(self) -> AsyncCasParserWithRawResponse: @@ -313,6 +512,7 @@ def copy( self, *, api_key: str | None = None, + environment: Literal["production", "environment_1", "environment_2"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, @@ -348,6 +548,7 @@ def copy( return self.__class__( api_key=api_key or self.api_key, base_url=base_url or self.base_url, + environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, @@ -401,10 +602,70 @@ def __init__(self, client: CasParser) -> None: self._client = client @cached_property - def cas_parser(self) -> cas_parser.CasParserResourceWithRawResponse: - from .resources.cas_parser import CasParserResourceWithRawResponse + def credits(self) -> credits.CreditsResourceWithRawResponse: + from .resources.credits import CreditsResourceWithRawResponse + + return CreditsResourceWithRawResponse(self._client.credits) + + @cached_property + def logs(self) -> logs.LogsResourceWithRawResponse: + from .resources.logs import LogsResourceWithRawResponse + + return LogsResourceWithRawResponse(self._client.logs) + + @cached_property + def access_token(self) -> access_token.AccessTokenResourceWithRawResponse: + from .resources.access_token import AccessTokenResourceWithRawResponse + + return AccessTokenResourceWithRawResponse(self._client.access_token) + + @cached_property + def verify_token(self) -> verify_token.VerifyTokenResourceWithRawResponse: + from .resources.verify_token import VerifyTokenResourceWithRawResponse + + return VerifyTokenResourceWithRawResponse(self._client.verify_token) + + @cached_property + def cams_kfintech(self) -> cams_kfintech.CamsKfintechResourceWithRawResponse: + from .resources.cams_kfintech import CamsKfintechResourceWithRawResponse + + return CamsKfintechResourceWithRawResponse(self._client.cams_kfintech) + + @cached_property + def cdsl(self) -> cdsl.CdslResourceWithRawResponse: + from .resources.cdsl import CdslResourceWithRawResponse + + return CdslResourceWithRawResponse(self._client.cdsl) - return CasParserResourceWithRawResponse(self._client.cas_parser) + @cached_property + def contract_note(self) -> contract_note.ContractNoteResourceWithRawResponse: + from .resources.contract_note import ContractNoteResourceWithRawResponse + + return ContractNoteResourceWithRawResponse(self._client.contract_note) + + @cached_property + def inbox(self) -> inbox.InboxResourceWithRawResponse: + from .resources.inbox import InboxResourceWithRawResponse + + return InboxResourceWithRawResponse(self._client.inbox) + + @cached_property + def kfintech(self) -> kfintech.KfintechResourceWithRawResponse: + from .resources.kfintech import KfintechResourceWithRawResponse + + return KfintechResourceWithRawResponse(self._client.kfintech) + + @cached_property + def nsdl(self) -> nsdl.NsdlResourceWithRawResponse: + from .resources.nsdl import NsdlResourceWithRawResponse + + return NsdlResourceWithRawResponse(self._client.nsdl) + + @cached_property + def smart(self) -> smart.SmartResourceWithRawResponse: + from .resources.smart import SmartResourceWithRawResponse + + return SmartResourceWithRawResponse(self._client.smart) class AsyncCasParserWithRawResponse: @@ -414,10 +675,70 @@ def __init__(self, client: AsyncCasParser) -> None: self._client = client @cached_property - def cas_parser(self) -> cas_parser.AsyncCasParserResourceWithRawResponse: - from .resources.cas_parser import AsyncCasParserResourceWithRawResponse + def credits(self) -> credits.AsyncCreditsResourceWithRawResponse: + from .resources.credits import AsyncCreditsResourceWithRawResponse + + return AsyncCreditsResourceWithRawResponse(self._client.credits) + + @cached_property + def logs(self) -> logs.AsyncLogsResourceWithRawResponse: + from .resources.logs import AsyncLogsResourceWithRawResponse + + return AsyncLogsResourceWithRawResponse(self._client.logs) + + @cached_property + def access_token(self) -> access_token.AsyncAccessTokenResourceWithRawResponse: + from .resources.access_token import AsyncAccessTokenResourceWithRawResponse + + return AsyncAccessTokenResourceWithRawResponse(self._client.access_token) + + @cached_property + def verify_token(self) -> verify_token.AsyncVerifyTokenResourceWithRawResponse: + from .resources.verify_token import AsyncVerifyTokenResourceWithRawResponse + + return AsyncVerifyTokenResourceWithRawResponse(self._client.verify_token) + + @cached_property + def cams_kfintech(self) -> cams_kfintech.AsyncCamsKfintechResourceWithRawResponse: + from .resources.cams_kfintech import AsyncCamsKfintechResourceWithRawResponse + + return AsyncCamsKfintechResourceWithRawResponse(self._client.cams_kfintech) + + @cached_property + def cdsl(self) -> cdsl.AsyncCdslResourceWithRawResponse: + from .resources.cdsl import AsyncCdslResourceWithRawResponse + + return AsyncCdslResourceWithRawResponse(self._client.cdsl) + + @cached_property + def contract_note(self) -> contract_note.AsyncContractNoteResourceWithRawResponse: + from .resources.contract_note import AsyncContractNoteResourceWithRawResponse + + return AsyncContractNoteResourceWithRawResponse(self._client.contract_note) + + @cached_property + def inbox(self) -> inbox.AsyncInboxResourceWithRawResponse: + from .resources.inbox import AsyncInboxResourceWithRawResponse - return AsyncCasParserResourceWithRawResponse(self._client.cas_parser) + return AsyncInboxResourceWithRawResponse(self._client.inbox) + + @cached_property + def kfintech(self) -> kfintech.AsyncKfintechResourceWithRawResponse: + from .resources.kfintech import AsyncKfintechResourceWithRawResponse + + return AsyncKfintechResourceWithRawResponse(self._client.kfintech) + + @cached_property + def nsdl(self) -> nsdl.AsyncNsdlResourceWithRawResponse: + from .resources.nsdl import AsyncNsdlResourceWithRawResponse + + return AsyncNsdlResourceWithRawResponse(self._client.nsdl) + + @cached_property + def smart(self) -> smart.AsyncSmartResourceWithRawResponse: + from .resources.smart import AsyncSmartResourceWithRawResponse + + return AsyncSmartResourceWithRawResponse(self._client.smart) class CasParserWithStreamedResponse: @@ -427,10 +748,70 @@ def __init__(self, client: CasParser) -> None: self._client = client @cached_property - def cas_parser(self) -> cas_parser.CasParserResourceWithStreamingResponse: - from .resources.cas_parser import CasParserResourceWithStreamingResponse + def credits(self) -> credits.CreditsResourceWithStreamingResponse: + from .resources.credits import CreditsResourceWithStreamingResponse + + return CreditsResourceWithStreamingResponse(self._client.credits) + + @cached_property + def logs(self) -> logs.LogsResourceWithStreamingResponse: + from .resources.logs import LogsResourceWithStreamingResponse + + return LogsResourceWithStreamingResponse(self._client.logs) + + @cached_property + def access_token(self) -> access_token.AccessTokenResourceWithStreamingResponse: + from .resources.access_token import AccessTokenResourceWithStreamingResponse + + return AccessTokenResourceWithStreamingResponse(self._client.access_token) + + @cached_property + def verify_token(self) -> verify_token.VerifyTokenResourceWithStreamingResponse: + from .resources.verify_token import VerifyTokenResourceWithStreamingResponse + + return VerifyTokenResourceWithStreamingResponse(self._client.verify_token) + + @cached_property + def cams_kfintech(self) -> cams_kfintech.CamsKfintechResourceWithStreamingResponse: + from .resources.cams_kfintech import CamsKfintechResourceWithStreamingResponse + + return CamsKfintechResourceWithStreamingResponse(self._client.cams_kfintech) + + @cached_property + def cdsl(self) -> cdsl.CdslResourceWithStreamingResponse: + from .resources.cdsl import CdslResourceWithStreamingResponse + + return CdslResourceWithStreamingResponse(self._client.cdsl) + + @cached_property + def contract_note(self) -> contract_note.ContractNoteResourceWithStreamingResponse: + from .resources.contract_note import ContractNoteResourceWithStreamingResponse + + return ContractNoteResourceWithStreamingResponse(self._client.contract_note) + + @cached_property + def inbox(self) -> inbox.InboxResourceWithStreamingResponse: + from .resources.inbox import InboxResourceWithStreamingResponse + + return InboxResourceWithStreamingResponse(self._client.inbox) + + @cached_property + def kfintech(self) -> kfintech.KfintechResourceWithStreamingResponse: + from .resources.kfintech import KfintechResourceWithStreamingResponse + + return KfintechResourceWithStreamingResponse(self._client.kfintech) - return CasParserResourceWithStreamingResponse(self._client.cas_parser) + @cached_property + def nsdl(self) -> nsdl.NsdlResourceWithStreamingResponse: + from .resources.nsdl import NsdlResourceWithStreamingResponse + + return NsdlResourceWithStreamingResponse(self._client.nsdl) + + @cached_property + def smart(self) -> smart.SmartResourceWithStreamingResponse: + from .resources.smart import SmartResourceWithStreamingResponse + + return SmartResourceWithStreamingResponse(self._client.smart) class AsyncCasParserWithStreamedResponse: @@ -440,10 +821,70 @@ def __init__(self, client: AsyncCasParser) -> None: self._client = client @cached_property - def cas_parser(self) -> cas_parser.AsyncCasParserResourceWithStreamingResponse: - from .resources.cas_parser import AsyncCasParserResourceWithStreamingResponse + def credits(self) -> credits.AsyncCreditsResourceWithStreamingResponse: + from .resources.credits import AsyncCreditsResourceWithStreamingResponse + + return AsyncCreditsResourceWithStreamingResponse(self._client.credits) + + @cached_property + def logs(self) -> logs.AsyncLogsResourceWithStreamingResponse: + from .resources.logs import AsyncLogsResourceWithStreamingResponse + + return AsyncLogsResourceWithStreamingResponse(self._client.logs) + + @cached_property + def access_token(self) -> access_token.AsyncAccessTokenResourceWithStreamingResponse: + from .resources.access_token import AsyncAccessTokenResourceWithStreamingResponse + + return AsyncAccessTokenResourceWithStreamingResponse(self._client.access_token) + + @cached_property + def verify_token(self) -> verify_token.AsyncVerifyTokenResourceWithStreamingResponse: + from .resources.verify_token import AsyncVerifyTokenResourceWithStreamingResponse + + return AsyncVerifyTokenResourceWithStreamingResponse(self._client.verify_token) + + @cached_property + def cams_kfintech(self) -> cams_kfintech.AsyncCamsKfintechResourceWithStreamingResponse: + from .resources.cams_kfintech import AsyncCamsKfintechResourceWithStreamingResponse + + return AsyncCamsKfintechResourceWithStreamingResponse(self._client.cams_kfintech) + + @cached_property + def cdsl(self) -> cdsl.AsyncCdslResourceWithStreamingResponse: + from .resources.cdsl import AsyncCdslResourceWithStreamingResponse + + return AsyncCdslResourceWithStreamingResponse(self._client.cdsl) + + @cached_property + def contract_note(self) -> contract_note.AsyncContractNoteResourceWithStreamingResponse: + from .resources.contract_note import AsyncContractNoteResourceWithStreamingResponse + + return AsyncContractNoteResourceWithStreamingResponse(self._client.contract_note) + + @cached_property + def inbox(self) -> inbox.AsyncInboxResourceWithStreamingResponse: + from .resources.inbox import AsyncInboxResourceWithStreamingResponse + + return AsyncInboxResourceWithStreamingResponse(self._client.inbox) + + @cached_property + def kfintech(self) -> kfintech.AsyncKfintechResourceWithStreamingResponse: + from .resources.kfintech import AsyncKfintechResourceWithStreamingResponse + + return AsyncKfintechResourceWithStreamingResponse(self._client.kfintech) + + @cached_property + def nsdl(self) -> nsdl.AsyncNsdlResourceWithStreamingResponse: + from .resources.nsdl import AsyncNsdlResourceWithStreamingResponse + + return AsyncNsdlResourceWithStreamingResponse(self._client.nsdl) + + @cached_property + def smart(self) -> smart.AsyncSmartResourceWithStreamingResponse: + from .resources.smart import AsyncSmartResourceWithStreamingResponse - return AsyncCasParserResourceWithStreamingResponse(self._client.cas_parser) + return AsyncSmartResourceWithStreamingResponse(self._client.smart) Client = CasParser diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 92a9acc..e9d03e0 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.2.1" # x-release-please-version +__version__ = "1.2.1" diff --git a/src/cas_parser/resources/__init__.py b/src/cas_parser/resources/__init__.py index 5da0162..ac91596 100644 --- a/src/cas_parser/resources/__init__.py +++ b/src/cas_parser/resources/__init__.py @@ -1,19 +1,159 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from .cas_parser import ( - CasParserResource, - AsyncCasParserResource, - CasParserResourceWithRawResponse, - AsyncCasParserResourceWithRawResponse, - CasParserResourceWithStreamingResponse, - AsyncCasParserResourceWithStreamingResponse, +from .cdsl import ( + CdslResource, + AsyncCdslResource, + CdslResourceWithRawResponse, + AsyncCdslResourceWithRawResponse, + CdslResourceWithStreamingResponse, + AsyncCdslResourceWithStreamingResponse, +) +from .logs import ( + LogsResource, + AsyncLogsResource, + LogsResourceWithRawResponse, + AsyncLogsResourceWithRawResponse, + LogsResourceWithStreamingResponse, + AsyncLogsResourceWithStreamingResponse, +) +from .nsdl import ( + NsdlResource, + AsyncNsdlResource, + NsdlResourceWithRawResponse, + AsyncNsdlResourceWithRawResponse, + NsdlResourceWithStreamingResponse, + AsyncNsdlResourceWithStreamingResponse, +) +from .inbox import ( + InboxResource, + AsyncInboxResource, + InboxResourceWithRawResponse, + AsyncInboxResourceWithRawResponse, + InboxResourceWithStreamingResponse, + AsyncInboxResourceWithStreamingResponse, +) +from .smart import ( + SmartResource, + AsyncSmartResource, + SmartResourceWithRawResponse, + AsyncSmartResourceWithRawResponse, + SmartResourceWithStreamingResponse, + AsyncSmartResourceWithStreamingResponse, +) +from .credits import ( + CreditsResource, + AsyncCreditsResource, + CreditsResourceWithRawResponse, + AsyncCreditsResourceWithRawResponse, + CreditsResourceWithStreamingResponse, + AsyncCreditsResourceWithStreamingResponse, +) +from .kfintech import ( + KfintechResource, + AsyncKfintechResource, + KfintechResourceWithRawResponse, + AsyncKfintechResourceWithRawResponse, + KfintechResourceWithStreamingResponse, + AsyncKfintechResourceWithStreamingResponse, +) +from .access_token import ( + AccessTokenResource, + AsyncAccessTokenResource, + AccessTokenResourceWithRawResponse, + AsyncAccessTokenResourceWithRawResponse, + AccessTokenResourceWithStreamingResponse, + AsyncAccessTokenResourceWithStreamingResponse, +) +from .verify_token import ( + VerifyTokenResource, + AsyncVerifyTokenResource, + VerifyTokenResourceWithRawResponse, + AsyncVerifyTokenResourceWithRawResponse, + VerifyTokenResourceWithStreamingResponse, + AsyncVerifyTokenResourceWithStreamingResponse, +) +from .cams_kfintech import ( + CamsKfintechResource, + AsyncCamsKfintechResource, + CamsKfintechResourceWithRawResponse, + AsyncCamsKfintechResourceWithRawResponse, + CamsKfintechResourceWithStreamingResponse, + AsyncCamsKfintechResourceWithStreamingResponse, +) +from .contract_note import ( + ContractNoteResource, + AsyncContractNoteResource, + ContractNoteResourceWithRawResponse, + AsyncContractNoteResourceWithRawResponse, + ContractNoteResourceWithStreamingResponse, + AsyncContractNoteResourceWithStreamingResponse, ) __all__ = [ - "CasParserResource", - "AsyncCasParserResource", - "CasParserResourceWithRawResponse", - "AsyncCasParserResourceWithRawResponse", - "CasParserResourceWithStreamingResponse", - "AsyncCasParserResourceWithStreamingResponse", + "CreditsResource", + "AsyncCreditsResource", + "CreditsResourceWithRawResponse", + "AsyncCreditsResourceWithRawResponse", + "CreditsResourceWithStreamingResponse", + "AsyncCreditsResourceWithStreamingResponse", + "LogsResource", + "AsyncLogsResource", + "LogsResourceWithRawResponse", + "AsyncLogsResourceWithRawResponse", + "LogsResourceWithStreamingResponse", + "AsyncLogsResourceWithStreamingResponse", + "AccessTokenResource", + "AsyncAccessTokenResource", + "AccessTokenResourceWithRawResponse", + "AsyncAccessTokenResourceWithRawResponse", + "AccessTokenResourceWithStreamingResponse", + "AsyncAccessTokenResourceWithStreamingResponse", + "VerifyTokenResource", + "AsyncVerifyTokenResource", + "VerifyTokenResourceWithRawResponse", + "AsyncVerifyTokenResourceWithRawResponse", + "VerifyTokenResourceWithStreamingResponse", + "AsyncVerifyTokenResourceWithStreamingResponse", + "CamsKfintechResource", + "AsyncCamsKfintechResource", + "CamsKfintechResourceWithRawResponse", + "AsyncCamsKfintechResourceWithRawResponse", + "CamsKfintechResourceWithStreamingResponse", + "AsyncCamsKfintechResourceWithStreamingResponse", + "CdslResource", + "AsyncCdslResource", + "CdslResourceWithRawResponse", + "AsyncCdslResourceWithRawResponse", + "CdslResourceWithStreamingResponse", + "AsyncCdslResourceWithStreamingResponse", + "ContractNoteResource", + "AsyncContractNoteResource", + "ContractNoteResourceWithRawResponse", + "AsyncContractNoteResourceWithRawResponse", + "ContractNoteResourceWithStreamingResponse", + "AsyncContractNoteResourceWithStreamingResponse", + "InboxResource", + "AsyncInboxResource", + "InboxResourceWithRawResponse", + "AsyncInboxResourceWithRawResponse", + "InboxResourceWithStreamingResponse", + "AsyncInboxResourceWithStreamingResponse", + "KfintechResource", + "AsyncKfintechResource", + "KfintechResourceWithRawResponse", + "AsyncKfintechResourceWithRawResponse", + "KfintechResourceWithStreamingResponse", + "AsyncKfintechResourceWithStreamingResponse", + "NsdlResource", + "AsyncNsdlResource", + "NsdlResourceWithRawResponse", + "AsyncNsdlResourceWithRawResponse", + "NsdlResourceWithStreamingResponse", + "AsyncNsdlResourceWithStreamingResponse", + "SmartResource", + "AsyncSmartResource", + "SmartResourceWithRawResponse", + "AsyncSmartResourceWithRawResponse", + "SmartResourceWithStreamingResponse", + "AsyncSmartResourceWithStreamingResponse", ] diff --git a/src/cas_parser/resources/access_token.py b/src/cas_parser/resources/access_token.py new file mode 100644 index 0000000..e8adeac --- /dev/null +++ b/src/cas_parser/resources/access_token.py @@ -0,0 +1,191 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import access_token_create_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.access_token_create_response import AccessTokenCreateResponse + +__all__ = ["AccessTokenResource", "AsyncAccessTokenResource"] + + +class AccessTokenResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> AccessTokenResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AccessTokenResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AccessTokenResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AccessTokenResourceWithStreamingResponse(self) + + def create( + self, + *, + expiry_minutes: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AccessTokenCreateResponse: + """ + Generate a short-lived access token from your API key. + + **Use this endpoint from your backend** to create tokens that can be safely + passed to frontend/SDK. + + Access tokens: + + - Are prefixed with `at_` for easy identification + - Valid for up to 60 minutes + - Can be used in place of API keys on all v4 endpoints + - Cannot be used to generate other access tokens + + Args: + expiry_minutes: Token validity in minutes (max 60) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/access-token", + body=maybe_transform( + {"expiry_minutes": expiry_minutes}, access_token_create_params.AccessTokenCreateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AccessTokenCreateResponse, + ) + + +class AsyncAccessTokenResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncAccessTokenResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncAccessTokenResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAccessTokenResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncAccessTokenResourceWithStreamingResponse(self) + + async def create( + self, + *, + expiry_minutes: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AccessTokenCreateResponse: + """ + Generate a short-lived access token from your API key. + + **Use this endpoint from your backend** to create tokens that can be safely + passed to frontend/SDK. + + Access tokens: + + - Are prefixed with `at_` for easy identification + - Valid for up to 60 minutes + - Can be used in place of API keys on all v4 endpoints + - Cannot be used to generate other access tokens + + Args: + expiry_minutes: Token validity in minutes (max 60) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/access-token", + body=await async_maybe_transform( + {"expiry_minutes": expiry_minutes}, access_token_create_params.AccessTokenCreateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AccessTokenCreateResponse, + ) + + +class AccessTokenResourceWithRawResponse: + def __init__(self, access_token: AccessTokenResource) -> None: + self._access_token = access_token + + self.create = to_raw_response_wrapper( + access_token.create, + ) + + +class AsyncAccessTokenResourceWithRawResponse: + def __init__(self, access_token: AsyncAccessTokenResource) -> None: + self._access_token = access_token + + self.create = async_to_raw_response_wrapper( + access_token.create, + ) + + +class AccessTokenResourceWithStreamingResponse: + def __init__(self, access_token: AccessTokenResource) -> None: + self._access_token = access_token + + self.create = to_streamed_response_wrapper( + access_token.create, + ) + + +class AsyncAccessTokenResourceWithStreamingResponse: + def __init__(self, access_token: AsyncAccessTokenResource) -> None: + self._access_token = access_token + + self.create = async_to_streamed_response_wrapper( + access_token.create, + ) diff --git a/src/cas_parser/resources/cams_kfintech.py b/src/cas_parser/resources/cams_kfintech.py new file mode 100644 index 0000000..63d7f7d --- /dev/null +++ b/src/cas_parser/resources/cams_kfintech.py @@ -0,0 +1,213 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast + +import httpx + +from ..types import cams_kfintech_parse_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.unified_response import UnifiedResponse + +__all__ = ["CamsKfintechResource", "AsyncCamsKfintechResource"] + + +class CamsKfintechResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CamsKfintechResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return CamsKfintechResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CamsKfintechResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return CamsKfintechResourceWithStreamingResponse(self) + + def parse( + self, + *, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> UnifiedResponse: + """ + This endpoint specifically parses CAMS/KFintech CAS (Consolidated Account + Statement) PDF files and returns data in a unified format. Use this endpoint + when you know the PDF is from CAMS or KFintech. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) + + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v4/cams_kfintech/parse", + body=maybe_transform(body, cams_kfintech_parse_params.CamsKfintechParseParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + +class AsyncCamsKfintechResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCamsKfintechResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncCamsKfintechResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCamsKfintechResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncCamsKfintechResourceWithStreamingResponse(self) + + async def parse( + self, + *, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> UnifiedResponse: + """ + This endpoint specifically parses CAMS/KFintech CAS (Consolidated Account + Statement) PDF files and returns data in a unified format. Use this endpoint + when you know the PDF is from CAMS or KFintech. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) + + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v4/cams_kfintech/parse", + body=await async_maybe_transform(body, cams_kfintech_parse_params.CamsKfintechParseParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + +class CamsKfintechResourceWithRawResponse: + def __init__(self, cams_kfintech: CamsKfintechResource) -> None: + self._cams_kfintech = cams_kfintech + + self.parse = to_raw_response_wrapper( + cams_kfintech.parse, + ) + + +class AsyncCamsKfintechResourceWithRawResponse: + def __init__(self, cams_kfintech: AsyncCamsKfintechResource) -> None: + self._cams_kfintech = cams_kfintech + + self.parse = async_to_raw_response_wrapper( + cams_kfintech.parse, + ) + + +class CamsKfintechResourceWithStreamingResponse: + def __init__(self, cams_kfintech: CamsKfintechResource) -> None: + self._cams_kfintech = cams_kfintech + + self.parse = to_streamed_response_wrapper( + cams_kfintech.parse, + ) + + +class AsyncCamsKfintechResourceWithStreamingResponse: + def __init__(self, cams_kfintech: AsyncCamsKfintechResource) -> None: + self._cams_kfintech = cams_kfintech + + self.parse = async_to_streamed_response_wrapper( + cams_kfintech.parse, + ) diff --git a/src/cas_parser/resources/cas_parser.py b/src/cas_parser/resources/cas_parser.py deleted file mode 100644 index 3a770a9..0000000 --- a/src/cas_parser/resources/cas_parser.py +++ /dev/null @@ -1,592 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Mapping, cast - -import httpx - -from ..types import ( - cas_parser_cdsl_params, - cas_parser_nsdl_params, - cas_parser_smart_parse_params, - cas_parser_cams_kfintech_params, -) -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.unified_response import UnifiedResponse - -__all__ = ["CasParserResource", "AsyncCasParserResource"] - - -class CasParserResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> CasParserResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return CasParserResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> CasParserResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return CasParserResourceWithStreamingResponse(self) - - def cams_kfintech( - self, - *, - password: str | Omit = omit, - pdf_file: str | Omit = omit, - pdf_url: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UnifiedResponse: - """ - This endpoint specifically parses CAMS/KFintech CAS (Consolidated Account - Statement) PDF files and returns data in a unified format. Use this endpoint - when you know the PDF is from CAMS or KFintech. - - Args: - password: Password for the PDF file (if required) - - pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - - pdf_url: URL to the CAS PDF file (required if pdf_file not provided) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "password": password, - "pdf_file": pdf_file, - "pdf_url": pdf_url, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) - if files: - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return self._post( - "/v4/cams_kfintech/parse", - body=maybe_transform(body, cas_parser_cams_kfintech_params.CasParserCamsKfintechParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=UnifiedResponse, - ) - - def cdsl( - self, - *, - password: str | Omit = omit, - pdf_file: str | Omit = omit, - pdf_url: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UnifiedResponse: - """ - This endpoint specifically parses CDSL CAS (Consolidated Account Statement) PDF - files and returns data in a unified format. Use this endpoint when you know the - PDF is from CDSL. - - Args: - password: Password for the PDF file (if required) - - pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - - pdf_url: URL to the CAS PDF file (required if pdf_file not provided) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "password": password, - "pdf_file": pdf_file, - "pdf_url": pdf_url, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) - if files: - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return self._post( - "/v4/cdsl/parse", - body=maybe_transform(body, cas_parser_cdsl_params.CasParserCdslParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=UnifiedResponse, - ) - - def nsdl( - self, - *, - password: str | Omit = omit, - pdf_file: str | Omit = omit, - pdf_url: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UnifiedResponse: - """ - This endpoint specifically parses NSDL CAS (Consolidated Account Statement) PDF - files and returns data in a unified format. Use this endpoint when you know the - PDF is from NSDL. - - Args: - password: Password for the PDF file (if required) - - pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - - pdf_url: URL to the CAS PDF file (required if pdf_file not provided) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "password": password, - "pdf_file": pdf_file, - "pdf_url": pdf_url, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) - if files: - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return self._post( - "/v4/nsdl/parse", - body=maybe_transform(body, cas_parser_nsdl_params.CasParserNsdlParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=UnifiedResponse, - ) - - def smart_parse( - self, - *, - password: str | Omit = omit, - pdf_file: str | Omit = omit, - pdf_url: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UnifiedResponse: - """ - This endpoint parses CAS (Consolidated Account Statement) PDF files from NSDL, - CDSL, or CAMS/KFintech and returns data in a unified format. It auto-detects the - CAS type and transforms the data into a consistent structure regardless of the - source. - - Args: - password: Password for the PDF file (if required) - - pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - - pdf_url: URL to the CAS PDF file (required if pdf_file not provided) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "password": password, - "pdf_file": pdf_file, - "pdf_url": pdf_url, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) - if files: - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return self._post( - "/v4/smart/parse", - body=maybe_transform(body, cas_parser_smart_parse_params.CasParserSmartParseParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=UnifiedResponse, - ) - - -class AsyncCasParserResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncCasParserResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return AsyncCasParserResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncCasParserResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return AsyncCasParserResourceWithStreamingResponse(self) - - async def cams_kfintech( - self, - *, - password: str | Omit = omit, - pdf_file: str | Omit = omit, - pdf_url: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UnifiedResponse: - """ - This endpoint specifically parses CAMS/KFintech CAS (Consolidated Account - Statement) PDF files and returns data in a unified format. Use this endpoint - when you know the PDF is from CAMS or KFintech. - - Args: - password: Password for the PDF file (if required) - - pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - - pdf_url: URL to the CAS PDF file (required if pdf_file not provided) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "password": password, - "pdf_file": pdf_file, - "pdf_url": pdf_url, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) - if files: - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return await self._post( - "/v4/cams_kfintech/parse", - body=await async_maybe_transform(body, cas_parser_cams_kfintech_params.CasParserCamsKfintechParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=UnifiedResponse, - ) - - async def cdsl( - self, - *, - password: str | Omit = omit, - pdf_file: str | Omit = omit, - pdf_url: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UnifiedResponse: - """ - This endpoint specifically parses CDSL CAS (Consolidated Account Statement) PDF - files and returns data in a unified format. Use this endpoint when you know the - PDF is from CDSL. - - Args: - password: Password for the PDF file (if required) - - pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - - pdf_url: URL to the CAS PDF file (required if pdf_file not provided) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "password": password, - "pdf_file": pdf_file, - "pdf_url": pdf_url, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) - if files: - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return await self._post( - "/v4/cdsl/parse", - body=await async_maybe_transform(body, cas_parser_cdsl_params.CasParserCdslParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=UnifiedResponse, - ) - - async def nsdl( - self, - *, - password: str | Omit = omit, - pdf_file: str | Omit = omit, - pdf_url: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UnifiedResponse: - """ - This endpoint specifically parses NSDL CAS (Consolidated Account Statement) PDF - files and returns data in a unified format. Use this endpoint when you know the - PDF is from NSDL. - - Args: - password: Password for the PDF file (if required) - - pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - - pdf_url: URL to the CAS PDF file (required if pdf_file not provided) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "password": password, - "pdf_file": pdf_file, - "pdf_url": pdf_url, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) - if files: - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return await self._post( - "/v4/nsdl/parse", - body=await async_maybe_transform(body, cas_parser_nsdl_params.CasParserNsdlParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=UnifiedResponse, - ) - - async def smart_parse( - self, - *, - password: str | Omit = omit, - pdf_file: str | Omit = omit, - pdf_url: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> UnifiedResponse: - """ - This endpoint parses CAS (Consolidated Account Statement) PDF files from NSDL, - CDSL, or CAMS/KFintech and returns data in a unified format. It auto-detects the - CAS type and transforms the data into a consistent structure regardless of the - source. - - Args: - password: Password for the PDF file (if required) - - pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) - - pdf_url: URL to the CAS PDF file (required if pdf_file not provided) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - body = deepcopy_minimal( - { - "password": password, - "pdf_file": pdf_file, - "pdf_url": pdf_url, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) - if files: - # It should be noted that the actual Content-Type header that will be - # sent to the server will contain a `boundary` parameter, e.g. - # multipart/form-data; boundary=---abc-- - extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return await self._post( - "/v4/smart/parse", - body=await async_maybe_transform(body, cas_parser_smart_parse_params.CasParserSmartParseParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=UnifiedResponse, - ) - - -class CasParserResourceWithRawResponse: - def __init__(self, cas_parser: CasParserResource) -> None: - self._cas_parser = cas_parser - - self.cams_kfintech = to_raw_response_wrapper( - cas_parser.cams_kfintech, - ) - self.cdsl = to_raw_response_wrapper( - cas_parser.cdsl, - ) - self.nsdl = to_raw_response_wrapper( - cas_parser.nsdl, - ) - self.smart_parse = to_raw_response_wrapper( - cas_parser.smart_parse, - ) - - -class AsyncCasParserResourceWithRawResponse: - def __init__(self, cas_parser: AsyncCasParserResource) -> None: - self._cas_parser = cas_parser - - self.cams_kfintech = async_to_raw_response_wrapper( - cas_parser.cams_kfintech, - ) - self.cdsl = async_to_raw_response_wrapper( - cas_parser.cdsl, - ) - self.nsdl = async_to_raw_response_wrapper( - cas_parser.nsdl, - ) - self.smart_parse = async_to_raw_response_wrapper( - cas_parser.smart_parse, - ) - - -class CasParserResourceWithStreamingResponse: - def __init__(self, cas_parser: CasParserResource) -> None: - self._cas_parser = cas_parser - - self.cams_kfintech = to_streamed_response_wrapper( - cas_parser.cams_kfintech, - ) - self.cdsl = to_streamed_response_wrapper( - cas_parser.cdsl, - ) - self.nsdl = to_streamed_response_wrapper( - cas_parser.nsdl, - ) - self.smart_parse = to_streamed_response_wrapper( - cas_parser.smart_parse, - ) - - -class AsyncCasParserResourceWithStreamingResponse: - def __init__(self, cas_parser: AsyncCasParserResource) -> None: - self._cas_parser = cas_parser - - self.cams_kfintech = async_to_streamed_response_wrapper( - cas_parser.cams_kfintech, - ) - self.cdsl = async_to_streamed_response_wrapper( - cas_parser.cdsl, - ) - self.nsdl = async_to_streamed_response_wrapper( - cas_parser.nsdl, - ) - self.smart_parse = async_to_streamed_response_wrapper( - cas_parser.smart_parse, - ) diff --git a/src/cas_parser/resources/cdsl/__init__.py b/src/cas_parser/resources/cdsl/__init__.py new file mode 100644 index 0000000..b11a9a6 --- /dev/null +++ b/src/cas_parser/resources/cdsl/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .cdsl import ( + CdslResource, + AsyncCdslResource, + CdslResourceWithRawResponse, + AsyncCdslResourceWithRawResponse, + CdslResourceWithStreamingResponse, + AsyncCdslResourceWithStreamingResponse, +) +from .fetch import ( + FetchResource, + AsyncFetchResource, + FetchResourceWithRawResponse, + AsyncFetchResourceWithRawResponse, + FetchResourceWithStreamingResponse, + AsyncFetchResourceWithStreamingResponse, +) + +__all__ = [ + "FetchResource", + "AsyncFetchResource", + "FetchResourceWithRawResponse", + "AsyncFetchResourceWithRawResponse", + "FetchResourceWithStreamingResponse", + "AsyncFetchResourceWithStreamingResponse", + "CdslResource", + "AsyncCdslResource", + "CdslResourceWithRawResponse", + "AsyncCdslResourceWithRawResponse", + "CdslResourceWithStreamingResponse", + "AsyncCdslResourceWithStreamingResponse", +] diff --git a/src/cas_parser/resources/cdsl/cdsl.py b/src/cas_parser/resources/cdsl/cdsl.py new file mode 100644 index 0000000..9e59381 --- /dev/null +++ b/src/cas_parser/resources/cdsl/cdsl.py @@ -0,0 +1,245 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast + +import httpx + +from .fetch import ( + FetchResource, + AsyncFetchResource, + FetchResourceWithRawResponse, + AsyncFetchResourceWithRawResponse, + FetchResourceWithStreamingResponse, + AsyncFetchResourceWithStreamingResponse, +) +from ...types import cdsl_parse_pdf_params +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.unified_response import UnifiedResponse + +__all__ = ["CdslResource", "AsyncCdslResource"] + + +class CdslResource(SyncAPIResource): + @cached_property + def fetch(self) -> FetchResource: + return FetchResource(self._client) + + @cached_property + def with_raw_response(self) -> CdslResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return CdslResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CdslResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return CdslResourceWithStreamingResponse(self) + + def parse_pdf( + self, + *, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> UnifiedResponse: + """ + This endpoint specifically parses CDSL CAS (Consolidated Account Statement) PDF + files and returns data in a unified format. Use this endpoint when you know the + PDF is from CDSL. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) + + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v4/cdsl/parse", + body=maybe_transform(body, cdsl_parse_pdf_params.CdslParsePdfParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + +class AsyncCdslResource(AsyncAPIResource): + @cached_property + def fetch(self) -> AsyncFetchResource: + return AsyncFetchResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncCdslResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncCdslResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCdslResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncCdslResourceWithStreamingResponse(self) + + async def parse_pdf( + self, + *, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> UnifiedResponse: + """ + This endpoint specifically parses CDSL CAS (Consolidated Account Statement) PDF + files and returns data in a unified format. Use this endpoint when you know the + PDF is from CDSL. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) + + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v4/cdsl/parse", + body=await async_maybe_transform(body, cdsl_parse_pdf_params.CdslParsePdfParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + +class CdslResourceWithRawResponse: + def __init__(self, cdsl: CdslResource) -> None: + self._cdsl = cdsl + + self.parse_pdf = to_raw_response_wrapper( + cdsl.parse_pdf, + ) + + @cached_property + def fetch(self) -> FetchResourceWithRawResponse: + return FetchResourceWithRawResponse(self._cdsl.fetch) + + +class AsyncCdslResourceWithRawResponse: + def __init__(self, cdsl: AsyncCdslResource) -> None: + self._cdsl = cdsl + + self.parse_pdf = async_to_raw_response_wrapper( + cdsl.parse_pdf, + ) + + @cached_property + def fetch(self) -> AsyncFetchResourceWithRawResponse: + return AsyncFetchResourceWithRawResponse(self._cdsl.fetch) + + +class CdslResourceWithStreamingResponse: + def __init__(self, cdsl: CdslResource) -> None: + self._cdsl = cdsl + + self.parse_pdf = to_streamed_response_wrapper( + cdsl.parse_pdf, + ) + + @cached_property + def fetch(self) -> FetchResourceWithStreamingResponse: + return FetchResourceWithStreamingResponse(self._cdsl.fetch) + + +class AsyncCdslResourceWithStreamingResponse: + def __init__(self, cdsl: AsyncCdslResource) -> None: + self._cdsl = cdsl + + self.parse_pdf = async_to_streamed_response_wrapper( + cdsl.parse_pdf, + ) + + @cached_property + def fetch(self) -> AsyncFetchResourceWithStreamingResponse: + return AsyncFetchResourceWithStreamingResponse(self._cdsl.fetch) diff --git a/src/cas_parser/resources/cdsl/fetch.py b/src/cas_parser/resources/cdsl/fetch.py new file mode 100644 index 0000000..85b7a1b --- /dev/null +++ b/src/cas_parser/resources/cdsl/fetch.py @@ -0,0 +1,322 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...types.cdsl import fetch_verify_otp_params, fetch_request_otp_params +from ..._base_client import make_request_options +from ...types.cdsl.fetch_verify_otp_response import FetchVerifyOtpResponse +from ...types.cdsl.fetch_request_otp_response import FetchRequestOtpResponse + +__all__ = ["FetchResource", "AsyncFetchResource"] + + +class FetchResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> FetchResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return FetchResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FetchResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return FetchResourceWithStreamingResponse(self) + + def request_otp( + self, + *, + bo_id: str, + dob: str, + pan: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> FetchRequestOtpResponse: + """**Step 1 of 2**: Request OTP for CDSL CAS fetch. + + This endpoint: + + 1. + + Solves reCAPTCHA automatically (~15-20 seconds) + 2. Submits login credentials to CDSL portal + 3. Triggers OTP to user's registered mobile number + + After user receives OTP, call `/v4/cdsl/fetch/{session_id}/verify` to complete. + + Args: + bo_id: CDSL BO ID (16 digits) + + dob: Date of birth (YYYY-MM-DD) + + pan: PAN number + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v4/cdsl/fetch", + body=maybe_transform( + { + "bo_id": bo_id, + "dob": dob, + "pan": pan, + }, + fetch_request_otp_params.FetchRequestOtpParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FetchRequestOtpResponse, + ) + + def verify_otp( + self, + session_id: str, + *, + otp: str, + num_periods: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> FetchVerifyOtpResponse: + """ + **Step 2 of 2**: Verify OTP and retrieve CDSL CAS files. + + After successful verification, CAS PDFs are fetched from CDSL portal, uploaded + to cloud storage, and returned as direct download URLs. + + Args: + otp: OTP received on mobile + + num_periods: Number of monthly statements to fetch (default 6) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + return self._post( + f"/v4/cdsl/fetch/{session_id}/verify", + body=maybe_transform( + { + "otp": otp, + "num_periods": num_periods, + }, + fetch_verify_otp_params.FetchVerifyOtpParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FetchVerifyOtpResponse, + ) + + +class AsyncFetchResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncFetchResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncFetchResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFetchResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncFetchResourceWithStreamingResponse(self) + + async def request_otp( + self, + *, + bo_id: str, + dob: str, + pan: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> FetchRequestOtpResponse: + """**Step 1 of 2**: Request OTP for CDSL CAS fetch. + + This endpoint: + + 1. + + Solves reCAPTCHA automatically (~15-20 seconds) + 2. Submits login credentials to CDSL portal + 3. Triggers OTP to user's registered mobile number + + After user receives OTP, call `/v4/cdsl/fetch/{session_id}/verify` to complete. + + Args: + bo_id: CDSL BO ID (16 digits) + + dob: Date of birth (YYYY-MM-DD) + + pan: PAN number + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v4/cdsl/fetch", + body=await async_maybe_transform( + { + "bo_id": bo_id, + "dob": dob, + "pan": pan, + }, + fetch_request_otp_params.FetchRequestOtpParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FetchRequestOtpResponse, + ) + + async def verify_otp( + self, + session_id: str, + *, + otp: str, + num_periods: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> FetchVerifyOtpResponse: + """ + **Step 2 of 2**: Verify OTP and retrieve CDSL CAS files. + + After successful verification, CAS PDFs are fetched from CDSL portal, uploaded + to cloud storage, and returned as direct download URLs. + + Args: + otp: OTP received on mobile + + num_periods: Number of monthly statements to fetch (default 6) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + return await self._post( + f"/v4/cdsl/fetch/{session_id}/verify", + body=await async_maybe_transform( + { + "otp": otp, + "num_periods": num_periods, + }, + fetch_verify_otp_params.FetchVerifyOtpParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FetchVerifyOtpResponse, + ) + + +class FetchResourceWithRawResponse: + def __init__(self, fetch: FetchResource) -> None: + self._fetch = fetch + + self.request_otp = to_raw_response_wrapper( + fetch.request_otp, + ) + self.verify_otp = to_raw_response_wrapper( + fetch.verify_otp, + ) + + +class AsyncFetchResourceWithRawResponse: + def __init__(self, fetch: AsyncFetchResource) -> None: + self._fetch = fetch + + self.request_otp = async_to_raw_response_wrapper( + fetch.request_otp, + ) + self.verify_otp = async_to_raw_response_wrapper( + fetch.verify_otp, + ) + + +class FetchResourceWithStreamingResponse: + def __init__(self, fetch: FetchResource) -> None: + self._fetch = fetch + + self.request_otp = to_streamed_response_wrapper( + fetch.request_otp, + ) + self.verify_otp = to_streamed_response_wrapper( + fetch.verify_otp, + ) + + +class AsyncFetchResourceWithStreamingResponse: + def __init__(self, fetch: AsyncFetchResource) -> None: + self._fetch = fetch + + self.request_otp = async_to_streamed_response_wrapper( + fetch.request_otp, + ) + self.verify_otp = async_to_streamed_response_wrapper( + fetch.verify_otp, + ) diff --git a/src/cas_parser/resources/contract_note.py b/src/cas_parser/resources/contract_note.py new file mode 100644 index 0000000..514e232 --- /dev/null +++ b/src/cas_parser/resources/contract_note.py @@ -0,0 +1,274 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast +from typing_extensions import Literal + +import httpx + +from ..types import contract_note_parse_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.contract_note_parse_response import ContractNoteParseResponse + +__all__ = ["ContractNoteResource", "AsyncContractNoteResource"] + + +class ContractNoteResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ContractNoteResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return ContractNoteResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ContractNoteResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return ContractNoteResourceWithStreamingResponse(self) + + def parse( + self, + *, + broker_type: Literal["zerodha", "groww", "upstox", "icici"] | Omit = omit, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContractNoteParseResponse: + """ + This endpoint parses Contract Note PDF files from various brokers including + Zerodha, Groww, Upstox, ICICI Securities, and others. + + **What is a Contract Note?** A contract note is a legal document that provides + details of all trades executed by an investor. It includes: + + - Trade details with timestamps, quantities, and prices + - Brokerage and charges breakdown + - Settlement information + - Regulatory compliance details + + **Supported Brokers:** + + - Zerodha Broking Limited + - Groww Invest Tech Private Limited + - Upstox (RKSV Securities) + - ICICI Securities Limited + - Auto-detection for unknown brokers + + **Key Features:** + + - **Auto-detection**: Automatically identifies broker type from PDF content + - **Comprehensive parsing**: Extracts equity transactions, derivatives + transactions, detailed trades, and charges + - **Flexible input**: Accepts both file upload and URL-based PDF input + - **Password protection**: Supports password-protected PDFs + + The API returns structured data including contract note information, client + details, transaction summaries, and detailed trade-by-trade breakdowns. + + Args: + broker_type: Optional broker type override. If not provided, system will auto-detect. + + password: Password for the PDF file (usually PAN number for Zerodha) + + pdf_file: Base64 encoded contract note PDF file + + pdf_url: URL to the contract note PDF file + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "broker_type": broker_type, + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v4/contract_note/parse", + body=maybe_transform(body, contract_note_parse_params.ContractNoteParseParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContractNoteParseResponse, + ) + + +class AsyncContractNoteResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncContractNoteResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncContractNoteResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncContractNoteResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncContractNoteResourceWithStreamingResponse(self) + + async def parse( + self, + *, + broker_type: Literal["zerodha", "groww", "upstox", "icici"] | Omit = omit, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContractNoteParseResponse: + """ + This endpoint parses Contract Note PDF files from various brokers including + Zerodha, Groww, Upstox, ICICI Securities, and others. + + **What is a Contract Note?** A contract note is a legal document that provides + details of all trades executed by an investor. It includes: + + - Trade details with timestamps, quantities, and prices + - Brokerage and charges breakdown + - Settlement information + - Regulatory compliance details + + **Supported Brokers:** + + - Zerodha Broking Limited + - Groww Invest Tech Private Limited + - Upstox (RKSV Securities) + - ICICI Securities Limited + - Auto-detection for unknown brokers + + **Key Features:** + + - **Auto-detection**: Automatically identifies broker type from PDF content + - **Comprehensive parsing**: Extracts equity transactions, derivatives + transactions, detailed trades, and charges + - **Flexible input**: Accepts both file upload and URL-based PDF input + - **Password protection**: Supports password-protected PDFs + + The API returns structured data including contract note information, client + details, transaction summaries, and detailed trade-by-trade breakdowns. + + Args: + broker_type: Optional broker type override. If not provided, system will auto-detect. + + password: Password for the PDF file (usually PAN number for Zerodha) + + pdf_file: Base64 encoded contract note PDF file + + pdf_url: URL to the contract note PDF file + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "broker_type": broker_type, + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v4/contract_note/parse", + body=await async_maybe_transform(body, contract_note_parse_params.ContractNoteParseParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContractNoteParseResponse, + ) + + +class ContractNoteResourceWithRawResponse: + def __init__(self, contract_note: ContractNoteResource) -> None: + self._contract_note = contract_note + + self.parse = to_raw_response_wrapper( + contract_note.parse, + ) + + +class AsyncContractNoteResourceWithRawResponse: + def __init__(self, contract_note: AsyncContractNoteResource) -> None: + self._contract_note = contract_note + + self.parse = async_to_raw_response_wrapper( + contract_note.parse, + ) + + +class ContractNoteResourceWithStreamingResponse: + def __init__(self, contract_note: ContractNoteResource) -> None: + self._contract_note = contract_note + + self.parse = to_streamed_response_wrapper( + contract_note.parse, + ) + + +class AsyncContractNoteResourceWithStreamingResponse: + def __init__(self, contract_note: AsyncContractNoteResource) -> None: + self._contract_note = contract_note + + self.parse = async_to_streamed_response_wrapper( + contract_note.parse, + ) diff --git a/src/cas_parser/resources/credits.py b/src/cas_parser/resources/credits.py new file mode 100644 index 0000000..8e66bc3 --- /dev/null +++ b/src/cas_parser/resources/credits.py @@ -0,0 +1,155 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .._types import Body, Query, Headers, NotGiven, not_given +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.credit_check_response import CreditCheckResponse + +__all__ = ["CreditsResource", "AsyncCreditsResource"] + + +class CreditsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CreditsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return CreditsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CreditsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return CreditsResourceWithStreamingResponse(self) + + def check( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CreditCheckResponse: + """ + Check your remaining API credits and usage for the current billing period. + + Returns: + + - Number of API calls used and remaining credits + - Credit limit and reset date + - List of enabled features for your plan + + Credits reset at the start of each billing period. + """ + return self._post( + "/credits", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CreditCheckResponse, + ) + + +class AsyncCreditsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCreditsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncCreditsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCreditsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncCreditsResourceWithStreamingResponse(self) + + async def check( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CreditCheckResponse: + """ + Check your remaining API credits and usage for the current billing period. + + Returns: + + - Number of API calls used and remaining credits + - Credit limit and reset date + - List of enabled features for your plan + + Credits reset at the start of each billing period. + """ + return await self._post( + "/credits", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CreditCheckResponse, + ) + + +class CreditsResourceWithRawResponse: + def __init__(self, credits: CreditsResource) -> None: + self._credits = credits + + self.check = to_raw_response_wrapper( + credits.check, + ) + + +class AsyncCreditsResourceWithRawResponse: + def __init__(self, credits: AsyncCreditsResource) -> None: + self._credits = credits + + self.check = async_to_raw_response_wrapper( + credits.check, + ) + + +class CreditsResourceWithStreamingResponse: + def __init__(self, credits: CreditsResource) -> None: + self._credits = credits + + self.check = to_streamed_response_wrapper( + credits.check, + ) + + +class AsyncCreditsResourceWithStreamingResponse: + def __init__(self, credits: AsyncCreditsResource) -> None: + self._credits = credits + + self.check = async_to_streamed_response_wrapper( + credits.check, + ) diff --git a/src/cas_parser/resources/inbox.py b/src/cas_parser/resources/inbox.py new file mode 100644 index 0000000..8ff5a4f --- /dev/null +++ b/src/cas_parser/resources/inbox.py @@ -0,0 +1,534 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from datetime import date +from typing_extensions import Literal + +import httpx + +from ..types import inbox_connect_email_params, inbox_list_cas_files_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.inbox_connect_email_response import InboxConnectEmailResponse +from ..types.inbox_list_cas_files_response import InboxListCasFilesResponse +from ..types.inbox_disconnect_email_response import InboxDisconnectEmailResponse +from ..types.inbox_check_connection_status_response import InboxCheckConnectionStatusResponse + +__all__ = ["InboxResource", "AsyncInboxResource"] + + +class InboxResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> InboxResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return InboxResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> InboxResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return InboxResourceWithStreamingResponse(self) + + def check_connection_status( + self, + *, + x_inbox_token: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboxCheckConnectionStatusResponse: + """ + Verify if an `inbox_token` is still valid and check connection status. + + Use this to check if the user needs to re-authenticate (e.g., if they revoked + access in their email provider settings). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"x-inbox-token": x_inbox_token, **(extra_headers or {})} + return self._post( + "/v4/inbox/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboxCheckConnectionStatusResponse, + ) + + def connect_email( + self, + *, + redirect_uri: str, + state: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboxConnectEmailResponse: + """ + Initiate OAuth flow to connect user's email inbox. + + Returns an `oauth_url` that you should redirect the user to. After + authorization, they are redirected back to your `redirect_uri` with the + following query parameters: + + **On success:** + + - `inbox_token` - Encrypted token to store client-side + - `email` - Email address of the connected account + - `state` - Your original state parameter (for CSRF verification) + + **On error:** + + - `error` - Error code (e.g., `access_denied`, `token_exchange_failed`) + - `state` - Your original state parameter + + **Store the `inbox_token` client-side** and use it for all subsequent inbox API + calls. + + Args: + redirect_uri: Your callback URL to receive the inbox_token (must be http or https) + + state: State parameter for CSRF protection (returned in redirect) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v4/inbox/connect", + body=maybe_transform( + { + "redirect_uri": redirect_uri, + "state": state, + }, + inbox_connect_email_params.InboxConnectEmailParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboxConnectEmailResponse, + ) + + def disconnect_email( + self, + *, + x_inbox_token: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboxDisconnectEmailResponse: + """ + Revoke email access and invalidate the token. + + This calls the provider's token revocation API (e.g., Google's revoke endpoint) + to ensure the user's consent is properly removed. + + After calling this, the `inbox_token` becomes unusable. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"x-inbox-token": x_inbox_token, **(extra_headers or {})} + return self._post( + "/v4/inbox/disconnect", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboxDisconnectEmailResponse, + ) + + def list_cas_files( + self, + *, + x_inbox_token: str, + cas_types: List[Literal["cdsl", "nsdl", "cams", "kfintech"]] | Omit = omit, + end_date: Union[str, date] | Omit = omit, + start_date: Union[str, date] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboxListCasFilesResponse: + """ + Search the user's email inbox for CAS files from known senders (CAMS, KFintech, + CDSL, NSDL). + + Files are uploaded to temporary cloud storage. **URLs expire in 24 hours.** + + Optionally filter by CAS provider and date range. + + **Billing:** 0.2 credits per request (charged regardless of success or number of + files found). + + Args: + cas_types: + Filter by CAS provider(s): + + - `cdsl` → eCAS@cdslstatement.com + - `nsdl` → NSDL-CAS@nsdl.co.in + - `cams` → donotreply@camsonline.com + - `kfintech` → samfS@kfintech.com + + end_date: End date in ISO format (YYYY-MM-DD). Defaults to today. + + start_date: Start date in ISO format (YYYY-MM-DD). Defaults to 30 days ago. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"x-inbox-token": x_inbox_token, **(extra_headers or {})} + return self._post( + "/v4/inbox/cas", + body=maybe_transform( + { + "cas_types": cas_types, + "end_date": end_date, + "start_date": start_date, + }, + inbox_list_cas_files_params.InboxListCasFilesParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboxListCasFilesResponse, + ) + + +class AsyncInboxResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncInboxResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncInboxResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncInboxResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncInboxResourceWithStreamingResponse(self) + + async def check_connection_status( + self, + *, + x_inbox_token: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboxCheckConnectionStatusResponse: + """ + Verify if an `inbox_token` is still valid and check connection status. + + Use this to check if the user needs to re-authenticate (e.g., if they revoked + access in their email provider settings). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"x-inbox-token": x_inbox_token, **(extra_headers or {})} + return await self._post( + "/v4/inbox/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboxCheckConnectionStatusResponse, + ) + + async def connect_email( + self, + *, + redirect_uri: str, + state: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboxConnectEmailResponse: + """ + Initiate OAuth flow to connect user's email inbox. + + Returns an `oauth_url` that you should redirect the user to. After + authorization, they are redirected back to your `redirect_uri` with the + following query parameters: + + **On success:** + + - `inbox_token` - Encrypted token to store client-side + - `email` - Email address of the connected account + - `state` - Your original state parameter (for CSRF verification) + + **On error:** + + - `error` - Error code (e.g., `access_denied`, `token_exchange_failed`) + - `state` - Your original state parameter + + **Store the `inbox_token` client-side** and use it for all subsequent inbox API + calls. + + Args: + redirect_uri: Your callback URL to receive the inbox_token (must be http or https) + + state: State parameter for CSRF protection (returned in redirect) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v4/inbox/connect", + body=await async_maybe_transform( + { + "redirect_uri": redirect_uri, + "state": state, + }, + inbox_connect_email_params.InboxConnectEmailParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboxConnectEmailResponse, + ) + + async def disconnect_email( + self, + *, + x_inbox_token: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboxDisconnectEmailResponse: + """ + Revoke email access and invalidate the token. + + This calls the provider's token revocation API (e.g., Google's revoke endpoint) + to ensure the user's consent is properly removed. + + After calling this, the `inbox_token` becomes unusable. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"x-inbox-token": x_inbox_token, **(extra_headers or {})} + return await self._post( + "/v4/inbox/disconnect", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboxDisconnectEmailResponse, + ) + + async def list_cas_files( + self, + *, + x_inbox_token: str, + cas_types: List[Literal["cdsl", "nsdl", "cams", "kfintech"]] | Omit = omit, + end_date: Union[str, date] | Omit = omit, + start_date: Union[str, date] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboxListCasFilesResponse: + """ + Search the user's email inbox for CAS files from known senders (CAMS, KFintech, + CDSL, NSDL). + + Files are uploaded to temporary cloud storage. **URLs expire in 24 hours.** + + Optionally filter by CAS provider and date range. + + **Billing:** 0.2 credits per request (charged regardless of success or number of + files found). + + Args: + cas_types: + Filter by CAS provider(s): + + - `cdsl` → eCAS@cdslstatement.com + - `nsdl` → NSDL-CAS@nsdl.co.in + - `cams` → donotreply@camsonline.com + - `kfintech` → samfS@kfintech.com + + end_date: End date in ISO format (YYYY-MM-DD). Defaults to today. + + start_date: Start date in ISO format (YYYY-MM-DD). Defaults to 30 days ago. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"x-inbox-token": x_inbox_token, **(extra_headers or {})} + return await self._post( + "/v4/inbox/cas", + body=await async_maybe_transform( + { + "cas_types": cas_types, + "end_date": end_date, + "start_date": start_date, + }, + inbox_list_cas_files_params.InboxListCasFilesParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboxListCasFilesResponse, + ) + + +class InboxResourceWithRawResponse: + def __init__(self, inbox: InboxResource) -> None: + self._inbox = inbox + + self.check_connection_status = to_raw_response_wrapper( + inbox.check_connection_status, + ) + self.connect_email = to_raw_response_wrapper( + inbox.connect_email, + ) + self.disconnect_email = to_raw_response_wrapper( + inbox.disconnect_email, + ) + self.list_cas_files = to_raw_response_wrapper( + inbox.list_cas_files, + ) + + +class AsyncInboxResourceWithRawResponse: + def __init__(self, inbox: AsyncInboxResource) -> None: + self._inbox = inbox + + self.check_connection_status = async_to_raw_response_wrapper( + inbox.check_connection_status, + ) + self.connect_email = async_to_raw_response_wrapper( + inbox.connect_email, + ) + self.disconnect_email = async_to_raw_response_wrapper( + inbox.disconnect_email, + ) + self.list_cas_files = async_to_raw_response_wrapper( + inbox.list_cas_files, + ) + + +class InboxResourceWithStreamingResponse: + def __init__(self, inbox: InboxResource) -> None: + self._inbox = inbox + + self.check_connection_status = to_streamed_response_wrapper( + inbox.check_connection_status, + ) + self.connect_email = to_streamed_response_wrapper( + inbox.connect_email, + ) + self.disconnect_email = to_streamed_response_wrapper( + inbox.disconnect_email, + ) + self.list_cas_files = to_streamed_response_wrapper( + inbox.list_cas_files, + ) + + +class AsyncInboxResourceWithStreamingResponse: + def __init__(self, inbox: AsyncInboxResource) -> None: + self._inbox = inbox + + self.check_connection_status = async_to_streamed_response_wrapper( + inbox.check_connection_status, + ) + self.connect_email = async_to_streamed_response_wrapper( + inbox.connect_email, + ) + self.disconnect_email = async_to_streamed_response_wrapper( + inbox.disconnect_email, + ) + self.list_cas_files = async_to_streamed_response_wrapper( + inbox.list_cas_files, + ) diff --git a/src/cas_parser/resources/kfintech.py b/src/cas_parser/resources/kfintech.py new file mode 100644 index 0000000..11ab9a5 --- /dev/null +++ b/src/cas_parser/resources/kfintech.py @@ -0,0 +1,219 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import kfintech_generate_cas_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.kfintech_generate_cas_response import KfintechGenerateCasResponse + +__all__ = ["KfintechResource", "AsyncKfintechResource"] + + +class KfintechResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> KfintechResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return KfintechResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> KfintechResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return KfintechResourceWithStreamingResponse(self) + + def generate_cas( + self, + *, + email: str, + from_date: str, + password: str, + to_date: str, + pan_no: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> KfintechGenerateCasResponse: + """Generate CAS via KFintech mailback. + + The CAS PDF will be sent to the investor's + email. + + This is an async operation - the investor receives the CAS via email within a + few minutes. For instant CAS retrieval, use CDSL Fetch (`/v4/cdsl/fetch`). + + Args: + email: Email address to receive the CAS document + + from_date: Start date (YYYY-MM-DD) + + password: Password for the PDF + + to_date: End date (YYYY-MM-DD) + + pan_no: PAN number (optional) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v4/kfintech/generate", + body=maybe_transform( + { + "email": email, + "from_date": from_date, + "password": password, + "to_date": to_date, + "pan_no": pan_no, + }, + kfintech_generate_cas_params.KfintechGenerateCasParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=KfintechGenerateCasResponse, + ) + + +class AsyncKfintechResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncKfintechResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncKfintechResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncKfintechResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncKfintechResourceWithStreamingResponse(self) + + async def generate_cas( + self, + *, + email: str, + from_date: str, + password: str, + to_date: str, + pan_no: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> KfintechGenerateCasResponse: + """Generate CAS via KFintech mailback. + + The CAS PDF will be sent to the investor's + email. + + This is an async operation - the investor receives the CAS via email within a + few minutes. For instant CAS retrieval, use CDSL Fetch (`/v4/cdsl/fetch`). + + Args: + email: Email address to receive the CAS document + + from_date: Start date (YYYY-MM-DD) + + password: Password for the PDF + + to_date: End date (YYYY-MM-DD) + + pan_no: PAN number (optional) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v4/kfintech/generate", + body=await async_maybe_transform( + { + "email": email, + "from_date": from_date, + "password": password, + "to_date": to_date, + "pan_no": pan_no, + }, + kfintech_generate_cas_params.KfintechGenerateCasParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=KfintechGenerateCasResponse, + ) + + +class KfintechResourceWithRawResponse: + def __init__(self, kfintech: KfintechResource) -> None: + self._kfintech = kfintech + + self.generate_cas = to_raw_response_wrapper( + kfintech.generate_cas, + ) + + +class AsyncKfintechResourceWithRawResponse: + def __init__(self, kfintech: AsyncKfintechResource) -> None: + self._kfintech = kfintech + + self.generate_cas = async_to_raw_response_wrapper( + kfintech.generate_cas, + ) + + +class KfintechResourceWithStreamingResponse: + def __init__(self, kfintech: KfintechResource) -> None: + self._kfintech = kfintech + + self.generate_cas = to_streamed_response_wrapper( + kfintech.generate_cas, + ) + + +class AsyncKfintechResourceWithStreamingResponse: + def __init__(self, kfintech: AsyncKfintechResource) -> None: + self._kfintech = kfintech + + self.generate_cas = async_to_streamed_response_wrapper( + kfintech.generate_cas, + ) diff --git a/src/cas_parser/resources/logs.py b/src/cas_parser/resources/logs.py new file mode 100644 index 0000000..9573ff7 --- /dev/null +++ b/src/cas_parser/resources/logs.py @@ -0,0 +1,307 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime + +import httpx + +from ..types import log_create_params, log_get_summary_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.log_create_response import LogCreateResponse +from ..types.log_get_summary_response import LogGetSummaryResponse + +__all__ = ["LogsResource", "AsyncLogsResource"] + + +class LogsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> LogsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return LogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return LogsResourceWithStreamingResponse(self) + + def create( + self, + *, + end_time: Union[str, datetime] | Omit = omit, + limit: int | Omit = omit, + start_time: Union[str, datetime] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogCreateResponse: + """ + Retrieve detailed API usage logs for your account. + + Returns a list of API calls with timestamps, features used, status codes, and + credits consumed. Useful for monitoring usage patterns and debugging. + + Args: + end_time: End time filter (ISO 8601). Defaults to now. + + limit: Maximum number of logs to return + + start_time: Start time filter (ISO 8601). Defaults to 30 days ago. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/logs", + body=maybe_transform( + { + "end_time": end_time, + "limit": limit, + "start_time": start_time, + }, + log_create_params.LogCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogCreateResponse, + ) + + def get_summary( + self, + *, + end_time: Union[str, datetime] | Omit = omit, + start_time: Union[str, datetime] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogGetSummaryResponse: + """ + Get aggregated usage statistics grouped by feature. + + Useful for understanding which API features are being used most and tracking + usage trends. + + Args: + end_time: End time filter (ISO 8601). Defaults to now. + + start_time: Start time filter (ISO 8601). Defaults to start of current month. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/logs/summary", + body=maybe_transform( + { + "end_time": end_time, + "start_time": start_time, + }, + log_get_summary_params.LogGetSummaryParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogGetSummaryResponse, + ) + + +class AsyncLogsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncLogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncLogsResourceWithStreamingResponse(self) + + async def create( + self, + *, + end_time: Union[str, datetime] | Omit = omit, + limit: int | Omit = omit, + start_time: Union[str, datetime] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogCreateResponse: + """ + Retrieve detailed API usage logs for your account. + + Returns a list of API calls with timestamps, features used, status codes, and + credits consumed. Useful for monitoring usage patterns and debugging. + + Args: + end_time: End time filter (ISO 8601). Defaults to now. + + limit: Maximum number of logs to return + + start_time: Start time filter (ISO 8601). Defaults to 30 days ago. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/logs", + body=await async_maybe_transform( + { + "end_time": end_time, + "limit": limit, + "start_time": start_time, + }, + log_create_params.LogCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogCreateResponse, + ) + + async def get_summary( + self, + *, + end_time: Union[str, datetime] | Omit = omit, + start_time: Union[str, datetime] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogGetSummaryResponse: + """ + Get aggregated usage statistics grouped by feature. + + Useful for understanding which API features are being used most and tracking + usage trends. + + Args: + end_time: End time filter (ISO 8601). Defaults to now. + + start_time: Start time filter (ISO 8601). Defaults to start of current month. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/logs/summary", + body=await async_maybe_transform( + { + "end_time": end_time, + "start_time": start_time, + }, + log_get_summary_params.LogGetSummaryParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogGetSummaryResponse, + ) + + +class LogsResourceWithRawResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.create = to_raw_response_wrapper( + logs.create, + ) + self.get_summary = to_raw_response_wrapper( + logs.get_summary, + ) + + +class AsyncLogsResourceWithRawResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.create = async_to_raw_response_wrapper( + logs.create, + ) + self.get_summary = async_to_raw_response_wrapper( + logs.get_summary, + ) + + +class LogsResourceWithStreamingResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.create = to_streamed_response_wrapper( + logs.create, + ) + self.get_summary = to_streamed_response_wrapper( + logs.get_summary, + ) + + +class AsyncLogsResourceWithStreamingResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.create = async_to_streamed_response_wrapper( + logs.create, + ) + self.get_summary = async_to_streamed_response_wrapper( + logs.get_summary, + ) diff --git a/src/cas_parser/resources/nsdl.py b/src/cas_parser/resources/nsdl.py new file mode 100644 index 0000000..9a8045f --- /dev/null +++ b/src/cas_parser/resources/nsdl.py @@ -0,0 +1,213 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast + +import httpx + +from ..types import nsdl_parse_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.unified_response import UnifiedResponse + +__all__ = ["NsdlResource", "AsyncNsdlResource"] + + +class NsdlResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> NsdlResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return NsdlResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> NsdlResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return NsdlResourceWithStreamingResponse(self) + + def parse( + self, + *, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> UnifiedResponse: + """ + This endpoint specifically parses NSDL CAS (Consolidated Account Statement) PDF + files and returns data in a unified format. Use this endpoint when you know the + PDF is from NSDL. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) + + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v4/nsdl/parse", + body=maybe_transform(body, nsdl_parse_params.NsdlParseParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + +class AsyncNsdlResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncNsdlResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncNsdlResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncNsdlResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncNsdlResourceWithStreamingResponse(self) + + async def parse( + self, + *, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> UnifiedResponse: + """ + This endpoint specifically parses NSDL CAS (Consolidated Account Statement) PDF + files and returns data in a unified format. Use this endpoint when you know the + PDF is from NSDL. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) + + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v4/nsdl/parse", + body=await async_maybe_transform(body, nsdl_parse_params.NsdlParseParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + +class NsdlResourceWithRawResponse: + def __init__(self, nsdl: NsdlResource) -> None: + self._nsdl = nsdl + + self.parse = to_raw_response_wrapper( + nsdl.parse, + ) + + +class AsyncNsdlResourceWithRawResponse: + def __init__(self, nsdl: AsyncNsdlResource) -> None: + self._nsdl = nsdl + + self.parse = async_to_raw_response_wrapper( + nsdl.parse, + ) + + +class NsdlResourceWithStreamingResponse: + def __init__(self, nsdl: NsdlResource) -> None: + self._nsdl = nsdl + + self.parse = to_streamed_response_wrapper( + nsdl.parse, + ) + + +class AsyncNsdlResourceWithStreamingResponse: + def __init__(self, nsdl: AsyncNsdlResource) -> None: + self._nsdl = nsdl + + self.parse = async_to_streamed_response_wrapper( + nsdl.parse, + ) diff --git a/src/cas_parser/resources/smart.py b/src/cas_parser/resources/smart.py new file mode 100644 index 0000000..9e1b0bc --- /dev/null +++ b/src/cas_parser/resources/smart.py @@ -0,0 +1,215 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast + +import httpx + +from ..types import smart_parse_cas_pdf_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.unified_response import UnifiedResponse + +__all__ = ["SmartResource", "AsyncSmartResource"] + + +class SmartResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> SmartResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return SmartResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SmartResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return SmartResourceWithStreamingResponse(self) + + def parse_cas_pdf( + self, + *, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> UnifiedResponse: + """ + This endpoint parses CAS (Consolidated Account Statement) PDF files from NSDL, + CDSL, or CAMS/KFintech and returns data in a unified format. It auto-detects the + CAS type and transforms the data into a consistent structure regardless of the + source. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) + + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v4/smart/parse", + body=maybe_transform(body, smart_parse_cas_pdf_params.SmartParseCasPdfParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + +class AsyncSmartResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncSmartResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncSmartResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSmartResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncSmartResourceWithStreamingResponse(self) + + async def parse_cas_pdf( + self, + *, + password: str | Omit = omit, + pdf_file: str | Omit = omit, + pdf_url: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> UnifiedResponse: + """ + This endpoint parses CAS (Consolidated Account Statement) PDF files from NSDL, + CDSL, or CAMS/KFintech and returns data in a unified format. It auto-detects the + CAS type and transforms the data into a consistent structure regardless of the + source. + + Args: + password: Password for the PDF file (if required) + + pdf_file: Base64 encoded CAS PDF file (required if pdf_url not provided) + + pdf_url: URL to the CAS PDF file (required if pdf_file not provided) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal( + { + "password": password, + "pdf_file": pdf_file, + "pdf_url": pdf_url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) + if files: + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v4/smart/parse", + body=await async_maybe_transform(body, smart_parse_cas_pdf_params.SmartParseCasPdfParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=UnifiedResponse, + ) + + +class SmartResourceWithRawResponse: + def __init__(self, smart: SmartResource) -> None: + self._smart = smart + + self.parse_cas_pdf = to_raw_response_wrapper( + smart.parse_cas_pdf, + ) + + +class AsyncSmartResourceWithRawResponse: + def __init__(self, smart: AsyncSmartResource) -> None: + self._smart = smart + + self.parse_cas_pdf = async_to_raw_response_wrapper( + smart.parse_cas_pdf, + ) + + +class SmartResourceWithStreamingResponse: + def __init__(self, smart: SmartResource) -> None: + self._smart = smart + + self.parse_cas_pdf = to_streamed_response_wrapper( + smart.parse_cas_pdf, + ) + + +class AsyncSmartResourceWithStreamingResponse: + def __init__(self, smart: AsyncSmartResource) -> None: + self._smart = smart + + self.parse_cas_pdf = async_to_streamed_response_wrapper( + smart.parse_cas_pdf, + ) diff --git a/src/cas_parser/resources/verify_token.py b/src/cas_parser/resources/verify_token.py new file mode 100644 index 0000000..ba3eeaa --- /dev/null +++ b/src/cas_parser/resources/verify_token.py @@ -0,0 +1,143 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .._types import Body, Query, Headers, NotGiven, not_given +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.verify_token_verify_response import VerifyTokenVerifyResponse + +__all__ = ["VerifyTokenResource", "AsyncVerifyTokenResource"] + + +class VerifyTokenResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> VerifyTokenResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return VerifyTokenResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> VerifyTokenResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return VerifyTokenResourceWithStreamingResponse(self) + + def verify( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> VerifyTokenVerifyResponse: + """Verify an access token and check if it's still valid. + + Useful for debugging token + issues. + """ + return self._post( + "/v1/verify-token", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=VerifyTokenVerifyResponse, + ) + + +class AsyncVerifyTokenResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncVerifyTokenResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncVerifyTokenResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncVerifyTokenResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + """ + return AsyncVerifyTokenResourceWithStreamingResponse(self) + + async def verify( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> VerifyTokenVerifyResponse: + """Verify an access token and check if it's still valid. + + Useful for debugging token + issues. + """ + return await self._post( + "/v1/verify-token", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=VerifyTokenVerifyResponse, + ) + + +class VerifyTokenResourceWithRawResponse: + def __init__(self, verify_token: VerifyTokenResource) -> None: + self._verify_token = verify_token + + self.verify = to_raw_response_wrapper( + verify_token.verify, + ) + + +class AsyncVerifyTokenResourceWithRawResponse: + def __init__(self, verify_token: AsyncVerifyTokenResource) -> None: + self._verify_token = verify_token + + self.verify = async_to_raw_response_wrapper( + verify_token.verify, + ) + + +class VerifyTokenResourceWithStreamingResponse: + def __init__(self, verify_token: VerifyTokenResource) -> None: + self._verify_token = verify_token + + self.verify = to_streamed_response_wrapper( + verify_token.verify, + ) + + +class AsyncVerifyTokenResourceWithStreamingResponse: + def __init__(self, verify_token: AsyncVerifyTokenResource) -> None: + self._verify_token = verify_token + + self.verify = async_to_streamed_response_wrapper( + verify_token.verify, + ) diff --git a/src/cas_parser/types/__init__.py b/src/cas_parser/types/__init__.py index fcdbc0b..d72939a 100644 --- a/src/cas_parser/types/__init__.py +++ b/src/cas_parser/types/__init__.py @@ -2,8 +2,30 @@ from __future__ import annotations +from .transaction import Transaction as Transaction +from .linked_holder import LinkedHolder as LinkedHolder from .unified_response import UnifiedResponse as UnifiedResponse -from .cas_parser_cdsl_params import CasParserCdslParams as CasParserCdslParams -from .cas_parser_nsdl_params import CasParserNsdlParams as CasParserNsdlParams -from .cas_parser_smart_parse_params import CasParserSmartParseParams as CasParserSmartParseParams -from .cas_parser_cams_kfintech_params import CasParserCamsKfintechParams as CasParserCamsKfintechParams +from .log_create_params import LogCreateParams as LogCreateParams +from .nsdl_parse_params import NsdlParseParams as NsdlParseParams +from .log_create_response import LogCreateResponse as LogCreateResponse +from .cdsl_parse_pdf_params import CdslParsePdfParams as CdslParsePdfParams +from .credit_check_response import CreditCheckResponse as CreditCheckResponse +from .log_get_summary_params import LogGetSummaryParams as LogGetSummaryParams +from .log_get_summary_response import LogGetSummaryResponse as LogGetSummaryResponse +from .access_token_create_params import AccessTokenCreateParams as AccessTokenCreateParams +from .cams_kfintech_parse_params import CamsKfintechParseParams as CamsKfintechParseParams +from .contract_note_parse_params import ContractNoteParseParams as ContractNoteParseParams +from .inbox_connect_email_params import InboxConnectEmailParams as InboxConnectEmailParams +from .smart_parse_cas_pdf_params import SmartParseCasPdfParams as SmartParseCasPdfParams +from .inbox_list_cas_files_params import InboxListCasFilesParams as InboxListCasFilesParams +from .access_token_create_response import AccessTokenCreateResponse as AccessTokenCreateResponse +from .contract_note_parse_response import ContractNoteParseResponse as ContractNoteParseResponse +from .inbox_connect_email_response import InboxConnectEmailResponse as InboxConnectEmailResponse +from .kfintech_generate_cas_params import KfintechGenerateCasParams as KfintechGenerateCasParams +from .verify_token_verify_response import VerifyTokenVerifyResponse as VerifyTokenVerifyResponse +from .inbox_list_cas_files_response import InboxListCasFilesResponse as InboxListCasFilesResponse +from .kfintech_generate_cas_response import KfintechGenerateCasResponse as KfintechGenerateCasResponse +from .inbox_disconnect_email_response import InboxDisconnectEmailResponse as InboxDisconnectEmailResponse +from .inbox_check_connection_status_response import ( + InboxCheckConnectionStatusResponse as InboxCheckConnectionStatusResponse, +) diff --git a/src/cas_parser/types/access_token_create_params.py b/src/cas_parser/types/access_token_create_params.py new file mode 100644 index 0000000..1d3f392 --- /dev/null +++ b/src/cas_parser/types/access_token_create_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["AccessTokenCreateParams"] + + +class AccessTokenCreateParams(TypedDict, total=False): + expiry_minutes: int + """Token validity in minutes (max 60)""" diff --git a/src/cas_parser/types/access_token_create_response.py b/src/cas_parser/types/access_token_create_response.py new file mode 100644 index 0000000..9a03c8c --- /dev/null +++ b/src/cas_parser/types/access_token_create_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["AccessTokenCreateResponse"] + + +class AccessTokenCreateResponse(BaseModel): + access_token: Optional[str] = None + """The at\\__ prefixed access token""" + + expires_in: Optional[int] = None + """Token validity in seconds""" + + token_type: Optional[str] = None + """Always "api_key" - token is a drop-in replacement for x-api-key header""" diff --git a/src/cas_parser/types/cas_parser_smart_parse_params.py b/src/cas_parser/types/cams_kfintech_parse_params.py similarity index 81% rename from src/cas_parser/types/cas_parser_smart_parse_params.py rename to src/cas_parser/types/cams_kfintech_parse_params.py index 56af64f..371e499 100644 --- a/src/cas_parser/types/cas_parser_smart_parse_params.py +++ b/src/cas_parser/types/cams_kfintech_parse_params.py @@ -4,10 +4,10 @@ from typing_extensions import TypedDict -__all__ = ["CasParserSmartParseParams"] +__all__ = ["CamsKfintechParseParams"] -class CasParserSmartParseParams(TypedDict, total=False): +class CamsKfintechParseParams(TypedDict, total=False): password: str """Password for the PDF file (if required)""" diff --git a/src/cas_parser/types/cdsl/__init__.py b/src/cas_parser/types/cdsl/__init__.py new file mode 100644 index 0000000..f85ec5c --- /dev/null +++ b/src/cas_parser/types/cdsl/__init__.py @@ -0,0 +1,8 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .fetch_verify_otp_params import FetchVerifyOtpParams as FetchVerifyOtpParams +from .fetch_request_otp_params import FetchRequestOtpParams as FetchRequestOtpParams +from .fetch_verify_otp_response import FetchVerifyOtpResponse as FetchVerifyOtpResponse +from .fetch_request_otp_response import FetchRequestOtpResponse as FetchRequestOtpResponse diff --git a/src/cas_parser/types/cdsl/fetch_request_otp_params.py b/src/cas_parser/types/cdsl/fetch_request_otp_params.py new file mode 100644 index 0000000..146d778 --- /dev/null +++ b/src/cas_parser/types/cdsl/fetch_request_otp_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FetchRequestOtpParams"] + + +class FetchRequestOtpParams(TypedDict, total=False): + bo_id: Required[str] + """CDSL BO ID (16 digits)""" + + dob: Required[str] + """Date of birth (YYYY-MM-DD)""" + + pan: Required[str] + """PAN number""" diff --git a/src/cas_parser/types/cdsl/fetch_request_otp_response.py b/src/cas_parser/types/cdsl/fetch_request_otp_response.py new file mode 100644 index 0000000..781f44a --- /dev/null +++ b/src/cas_parser/types/cdsl/fetch_request_otp_response.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["FetchRequestOtpResponse"] + + +class FetchRequestOtpResponse(BaseModel): + msg: Optional[str] = None + + session_id: Optional[str] = None + """Session ID for verify step""" + + status: Optional[str] = None diff --git a/src/cas_parser/types/cdsl/fetch_verify_otp_params.py b/src/cas_parser/types/cdsl/fetch_verify_otp_params.py new file mode 100644 index 0000000..3ff31e2 --- /dev/null +++ b/src/cas_parser/types/cdsl/fetch_verify_otp_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FetchVerifyOtpParams"] + + +class FetchVerifyOtpParams(TypedDict, total=False): + otp: Required[str] + """OTP received on mobile""" + + num_periods: int + """Number of monthly statements to fetch (default 6)""" diff --git a/src/cas_parser/types/cdsl/fetch_verify_otp_response.py b/src/cas_parser/types/cdsl/fetch_verify_otp_response.py new file mode 100644 index 0000000..6791ed5 --- /dev/null +++ b/src/cas_parser/types/cdsl/fetch_verify_otp_response.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel + +__all__ = ["FetchVerifyOtpResponse", "File"] + + +class File(BaseModel): + filename: Optional[str] = None + + url: Optional[str] = None + """Direct download URL (cloud storage)""" + + +class FetchVerifyOtpResponse(BaseModel): + files: Optional[List[File]] = None + + msg: Optional[str] = None + + status: Optional[str] = None diff --git a/src/cas_parser/types/cas_parser_cdsl_params.py b/src/cas_parser/types/cdsl_parse_pdf_params.py similarity index 82% rename from src/cas_parser/types/cas_parser_cdsl_params.py rename to src/cas_parser/types/cdsl_parse_pdf_params.py index 725dc6d..06ef09b 100644 --- a/src/cas_parser/types/cas_parser_cdsl_params.py +++ b/src/cas_parser/types/cdsl_parse_pdf_params.py @@ -4,10 +4,10 @@ from typing_extensions import TypedDict -__all__ = ["CasParserCdslParams"] +__all__ = ["CdslParsePdfParams"] -class CasParserCdslParams(TypedDict, total=False): +class CdslParsePdfParams(TypedDict, total=False): password: str """Password for the PDF file (if required)""" diff --git a/src/cas_parser/types/contract_note_parse_params.py b/src/cas_parser/types/contract_note_parse_params.py new file mode 100644 index 0000000..05dcda1 --- /dev/null +++ b/src/cas_parser/types/contract_note_parse_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["ContractNoteParseParams"] + + +class ContractNoteParseParams(TypedDict, total=False): + broker_type: Literal["zerodha", "groww", "upstox", "icici"] + """Optional broker type override. If not provided, system will auto-detect.""" + + password: str + """Password for the PDF file (usually PAN number for Zerodha)""" + + pdf_file: str + """Base64 encoded contract note PDF file""" + + pdf_url: str + """URL to the contract note PDF file""" diff --git a/src/cas_parser/types/contract_note_parse_response.py b/src/cas_parser/types/contract_note_parse_response.py new file mode 100644 index 0000000..7f5183d --- /dev/null +++ b/src/cas_parser/types/contract_note_parse_response.py @@ -0,0 +1,222 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import date +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = [ + "ContractNoteParseResponse", + "Data", + "DataBrokerInfo", + "DataChargesSummary", + "DataClientInfo", + "DataContractNoteInfo", + "DataDerivativesTransaction", + "DataDetailedTrade", + "DataEquityTransaction", +] + + +class DataBrokerInfo(BaseModel): + broker_type: Optional[Literal["zerodha", "groww", "upstox", "icici", "unknown"]] = None + """Auto-detected or specified broker type""" + + name: Optional[str] = None + """Broker company name""" + + sebi_registration: Optional[str] = None + """SEBI registration number of the broker""" + + +class DataChargesSummary(BaseModel): + """Breakdown of various charges and fees""" + + cgst: Optional[float] = None + """Central GST amount""" + + exchange_transaction_charges: Optional[float] = None + """Exchange transaction charges""" + + igst: Optional[float] = None + """Integrated GST amount""" + + net_amount_receivable_payable: Optional[float] = None + """Final net amount receivable or payable""" + + pay_in_pay_out_obligation: Optional[float] = None + """Net pay-in/pay-out obligation""" + + sebi_turnover_fees: Optional[float] = None + """SEBI turnover fees""" + + securities_transaction_tax: Optional[float] = None + """Securities Transaction Tax""" + + sgst: Optional[float] = None + """State GST amount""" + + stamp_duty: Optional[float] = None + """Stamp duty charges""" + + taxable_value_brokerage: Optional[float] = None + """Taxable brokerage amount""" + + +class DataClientInfo(BaseModel): + address: Optional[str] = None + """Client address""" + + gst_state_code: Optional[str] = None + """GST state code""" + + name: Optional[str] = None + """Client name""" + + pan: Optional[str] = None + """Client PAN number""" + + place_of_supply: Optional[str] = None + """GST place of supply""" + + ucc: Optional[str] = None + """Unique Client Code""" + + +class DataContractNoteInfo(BaseModel): + contract_note_number: Optional[str] = None + """Contract note reference number""" + + settlement_date: Optional[date] = None + """Settlement date for the trades""" + + settlement_number: Optional[str] = None + """Settlement reference number""" + + trade_date: Optional[date] = None + """Date when trades were executed""" + + +class DataDerivativesTransaction(BaseModel): + brokerage_per_unit: Optional[float] = None + """Brokerage charged per unit""" + + buy_sell_bf_cf: Optional[str] = None + """Transaction type (Buy/Sell/Bring Forward/Carry Forward)""" + + closing_rate_per_unit: Optional[float] = None + """Closing rate per unit""" + + contract_description: Optional[str] = None + """Derivatives contract description""" + + net_total: Optional[float] = None + """Net total amount""" + + quantity: Optional[float] = None + """Quantity traded""" + + wap_per_unit: Optional[float] = None + """Weighted Average Price per unit""" + + +class DataDetailedTrade(BaseModel): + brokerage: Optional[float] = None + """Brokerage charged for this trade""" + + buy_sell: Optional[str] = None + """Transaction type (B for Buy, S for Sell)""" + + closing_rate_per_unit: Optional[float] = None + """Closing rate per unit""" + + exchange: Optional[str] = None + """Exchange name""" + + net_rate_per_unit: Optional[float] = None + """Net rate per unit""" + + net_total: Optional[float] = None + """Net total for this trade""" + + order_number: Optional[str] = None + """Order reference number""" + + order_time: Optional[str] = None + """Time when order was placed""" + + quantity: Optional[float] = None + """Quantity traded""" + + remarks: Optional[str] = None + """Additional remarks or notes""" + + security_description: Optional[str] = None + """Security name with exchange and ISIN""" + + trade_number: Optional[str] = None + """Trade reference number""" + + trade_time: Optional[str] = None + """Time when trade was executed""" + + +class DataEquityTransaction(BaseModel): + buy_quantity: Optional[float] = None + """Total quantity purchased""" + + buy_total_value: Optional[float] = None + """Total value of buy transactions""" + + buy_wap: Optional[float] = None + """Weighted Average Price for buy transactions""" + + isin: Optional[str] = None + """ISIN code of the security""" + + net_obligation: Optional[float] = None + """Net amount payable/receivable for this security""" + + security_name: Optional[str] = None + """Name of the security""" + + security_symbol: Optional[str] = None + """Trading symbol""" + + sell_quantity: Optional[float] = None + """Total quantity sold""" + + sell_total_value: Optional[float] = None + """Total value of sell transactions""" + + sell_wap: Optional[float] = None + """Weighted Average Price for sell transactions""" + + +class Data(BaseModel): + broker_info: Optional[DataBrokerInfo] = None + + charges_summary: Optional[DataChargesSummary] = None + """Breakdown of various charges and fees""" + + client_info: Optional[DataClientInfo] = None + + contract_note_info: Optional[DataContractNoteInfo] = None + + derivatives_transactions: Optional[List[DataDerivativesTransaction]] = None + """Summary of derivatives transactions""" + + detailed_trades: Optional[List[DataDetailedTrade]] = None + """Detailed breakdown of all individual trades""" + + equity_transactions: Optional[List[DataEquityTransaction]] = None + """Summary of equity transactions grouped by security""" + + +class ContractNoteParseResponse(BaseModel): + data: Optional[Data] = None + + msg: Optional[str] = None + + status: Optional[str] = None diff --git a/src/cas_parser/types/credit_check_response.py b/src/cas_parser/types/credit_check_response.py new file mode 100644 index 0000000..9396b9f --- /dev/null +++ b/src/cas_parser/types/credit_check_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["CreditCheckResponse"] + + +class CreditCheckResponse(BaseModel): + enabled_features: Optional[List[str]] = None + """List of API features enabled for your plan""" + + is_unlimited: Optional[bool] = None + """Whether the account has unlimited credits""" + + limit: Optional[int] = None + """Total credit limit for billing period""" + + remaining: Optional[float] = None + """Remaining credits (null if unlimited)""" + + resets_at: Optional[datetime] = None + """When credits reset (ISO 8601)""" + + used: Optional[float] = None + """Number of credits used this billing period""" diff --git a/src/cas_parser/types/inbox_check_connection_status_response.py b/src/cas_parser/types/inbox_check_connection_status_response.py new file mode 100644 index 0000000..c1be3c7 --- /dev/null +++ b/src/cas_parser/types/inbox_check_connection_status_response.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["InboxCheckConnectionStatusResponse"] + + +class InboxCheckConnectionStatusResponse(BaseModel): + connected: Optional[bool] = None + """Whether the token is valid and usable""" + + email: Optional[str] = None + """Email address of the connected account""" + + provider: Optional[str] = None + + status: Optional[str] = None diff --git a/src/cas_parser/types/inbox_connect_email_params.py b/src/cas_parser/types/inbox_connect_email_params.py new file mode 100644 index 0000000..73b6ec6 --- /dev/null +++ b/src/cas_parser/types/inbox_connect_email_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["InboxConnectEmailParams"] + + +class InboxConnectEmailParams(TypedDict, total=False): + redirect_uri: Required[str] + """Your callback URL to receive the inbox_token (must be http or https)""" + + state: str + """State parameter for CSRF protection (returned in redirect)""" diff --git a/src/cas_parser/types/inbox_connect_email_response.py b/src/cas_parser/types/inbox_connect_email_response.py new file mode 100644 index 0000000..7062217 --- /dev/null +++ b/src/cas_parser/types/inbox_connect_email_response.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["InboxConnectEmailResponse"] + + +class InboxConnectEmailResponse(BaseModel): + expires_in: Optional[int] = None + """Seconds until the OAuth URL expires (typically 10 minutes)""" + + oauth_url: Optional[str] = None + """Redirect user to this URL to start OAuth flow""" + + status: Optional[str] = None diff --git a/src/cas_parser/types/inbox_disconnect_email_response.py b/src/cas_parser/types/inbox_disconnect_email_response.py new file mode 100644 index 0000000..487a32d --- /dev/null +++ b/src/cas_parser/types/inbox_disconnect_email_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["InboxDisconnectEmailResponse"] + + +class InboxDisconnectEmailResponse(BaseModel): + msg: Optional[str] = None + + status: Optional[str] = None diff --git a/src/cas_parser/types/inbox_list_cas_files_params.py b/src/cas_parser/types/inbox_list_cas_files_params.py new file mode 100644 index 0000000..e178160 --- /dev/null +++ b/src/cas_parser/types/inbox_list_cas_files_params.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Union +from datetime import date +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["InboxListCasFilesParams"] + + +class InboxListCasFilesParams(TypedDict, total=False): + x_inbox_token: Required[Annotated[str, PropertyInfo(alias="x-inbox-token")]] + + cas_types: List[Literal["cdsl", "nsdl", "cams", "kfintech"]] + """Filter by CAS provider(s): + + - `cdsl` → eCAS@cdslstatement.com + - `nsdl` → NSDL-CAS@nsdl.co.in + - `cams` → donotreply@camsonline.com + - `kfintech` → samfS@kfintech.com + """ + + end_date: Annotated[Union[str, date], PropertyInfo(format="iso8601")] + """End date in ISO format (YYYY-MM-DD). Defaults to today.""" + + start_date: Annotated[Union[str, date], PropertyInfo(format="iso8601")] + """Start date in ISO format (YYYY-MM-DD). Defaults to 30 days ago.""" diff --git a/src/cas_parser/types/inbox_list_cas_files_response.py b/src/cas_parser/types/inbox_list_cas_files_response.py new file mode 100644 index 0000000..ef27d92 --- /dev/null +++ b/src/cas_parser/types/inbox_list_cas_files_response.py @@ -0,0 +1,46 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import date +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["InboxListCasFilesResponse", "File"] + + +class File(BaseModel): + """A CAS file found in the user's email inbox""" + + cas_type: Optional[Literal["cdsl", "nsdl", "cams", "kfintech"]] = None + """Detected CAS provider based on sender email""" + + expires_in: Optional[int] = None + """URL expiration time in seconds (default 86400 = 24 hours)""" + + filename: Optional[str] = None + """Standardized filename (provider_YYYYMMDD_uniqueid.pdf)""" + + message_date: Optional[date] = None + """Date the email was received""" + + message_id: Optional[str] = None + """Unique identifier for the email message (use for subsequent API calls)""" + + original_filename: Optional[str] = None + """Original attachment filename from the email""" + + size: Optional[int] = None + """File size in bytes""" + + url: Optional[str] = None + """Direct download URL (presigned, expires based on expires_in)""" + + +class InboxListCasFilesResponse(BaseModel): + count: Optional[int] = None + """Number of CAS files found""" + + files: Optional[List[File]] = None + + status: Optional[str] = None diff --git a/src/cas_parser/types/kfintech_generate_cas_params.py b/src/cas_parser/types/kfintech_generate_cas_params.py new file mode 100644 index 0000000..7c16d71 --- /dev/null +++ b/src/cas_parser/types/kfintech_generate_cas_params.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["KfintechGenerateCasParams"] + + +class KfintechGenerateCasParams(TypedDict, total=False): + email: Required[str] + """Email address to receive the CAS document""" + + from_date: Required[str] + """Start date (YYYY-MM-DD)""" + + password: Required[str] + """Password for the PDF""" + + to_date: Required[str] + """End date (YYYY-MM-DD)""" + + pan_no: str + """PAN number (optional)""" diff --git a/src/cas_parser/types/kfintech_generate_cas_response.py b/src/cas_parser/types/kfintech_generate_cas_response.py new file mode 100644 index 0000000..9d281ef --- /dev/null +++ b/src/cas_parser/types/kfintech_generate_cas_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["KfintechGenerateCasResponse"] + + +class KfintechGenerateCasResponse(BaseModel): + msg: Optional[str] = None + + status: Optional[str] = None diff --git a/src/cas_parser/types/linked_holder.py b/src/cas_parser/types/linked_holder.py new file mode 100644 index 0000000..f1c84a9 --- /dev/null +++ b/src/cas_parser/types/linked_holder.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["LinkedHolder"] + + +class LinkedHolder(BaseModel): + name: Optional[str] = None + """Name of the account holder""" + + pan: Optional[str] = None + """PAN of the account holder""" diff --git a/src/cas_parser/types/log_create_params.py b/src/cas_parser/types/log_create_params.py new file mode 100644 index 0000000..6104297 --- /dev/null +++ b/src/cas_parser/types/log_create_params.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["LogCreateParams"] + + +class LogCreateParams(TypedDict, total=False): + end_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """End time filter (ISO 8601). Defaults to now.""" + + limit: int + """Maximum number of logs to return""" + + start_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """Start time filter (ISO 8601). Defaults to 30 days ago.""" diff --git a/src/cas_parser/types/log_create_response.py b/src/cas_parser/types/log_create_response.py new file mode 100644 index 0000000..446d6e5 --- /dev/null +++ b/src/cas_parser/types/log_create_response.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["LogCreateResponse", "Log"] + + +class Log(BaseModel): + credits: Optional[float] = None + """Credits consumed for this request""" + + feature: Optional[str] = None + """API feature used""" + + path: Optional[str] = None + """API endpoint path""" + + request_id: Optional[str] = None + """Unique request identifier""" + + status_code: Optional[int] = None + """HTTP response status code""" + + timestamp: Optional[datetime] = None + """When the request was made""" + + +class LogCreateResponse(BaseModel): + count: Optional[int] = None + """Number of logs returned""" + + logs: Optional[List[Log]] = None + + status: Optional[str] = None diff --git a/src/cas_parser/types/log_get_summary_params.py b/src/cas_parser/types/log_get_summary_params.py new file mode 100644 index 0000000..fc9ffe7 --- /dev/null +++ b/src/cas_parser/types/log_get_summary_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["LogGetSummaryParams"] + + +class LogGetSummaryParams(TypedDict, total=False): + end_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """End time filter (ISO 8601). Defaults to now.""" + + start_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """Start time filter (ISO 8601). Defaults to start of current month.""" diff --git a/src/cas_parser/types/log_get_summary_response.py b/src/cas_parser/types/log_get_summary_response.py new file mode 100644 index 0000000..d947f84 --- /dev/null +++ b/src/cas_parser/types/log_get_summary_response.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["LogGetSummaryResponse", "Summary", "SummaryByFeature"] + + +class SummaryByFeature(BaseModel): + credits: Optional[float] = None + """Credits consumed by this feature""" + + feature: Optional[str] = None + """API feature name""" + + requests: Optional[int] = None + """Number of requests for this feature""" + + +class Summary(BaseModel): + by_feature: Optional[List[SummaryByFeature]] = None + """Usage breakdown by feature""" + + total_credits: Optional[float] = None + """Total credits consumed in the period""" + + total_requests: Optional[int] = None + """Total API requests made in the period""" + + +class LogGetSummaryResponse(BaseModel): + status: Optional[str] = None + + summary: Optional[Summary] = None diff --git a/src/cas_parser/types/cas_parser_nsdl_params.py b/src/cas_parser/types/nsdl_parse_params.py similarity index 82% rename from src/cas_parser/types/cas_parser_nsdl_params.py rename to src/cas_parser/types/nsdl_parse_params.py index 357f533..a56ec45 100644 --- a/src/cas_parser/types/cas_parser_nsdl_params.py +++ b/src/cas_parser/types/nsdl_parse_params.py @@ -4,10 +4,10 @@ from typing_extensions import TypedDict -__all__ = ["CasParserNsdlParams"] +__all__ = ["NsdlParseParams"] -class CasParserNsdlParams(TypedDict, total=False): +class NsdlParseParams(TypedDict, total=False): password: str """Password for the PDF file (if required)""" diff --git a/src/cas_parser/types/cas_parser_cams_kfintech_params.py b/src/cas_parser/types/smart_parse_cas_pdf_params.py similarity index 80% rename from src/cas_parser/types/cas_parser_cams_kfintech_params.py rename to src/cas_parser/types/smart_parse_cas_pdf_params.py index f0a1552..b830a6e 100644 --- a/src/cas_parser/types/cas_parser_cams_kfintech_params.py +++ b/src/cas_parser/types/smart_parse_cas_pdf_params.py @@ -4,10 +4,10 @@ from typing_extensions import TypedDict -__all__ = ["CasParserCamsKfintechParams"] +__all__ = ["SmartParseCasPdfParams"] -class CasParserCamsKfintechParams(TypedDict, total=False): +class SmartParseCasPdfParams(TypedDict, total=False): password: str """Password for the PDF file (if required)""" diff --git a/src/cas_parser/types/transaction.py b/src/cas_parser/types/transaction.py new file mode 100644 index 0000000..6804a5d --- /dev/null +++ b/src/cas_parser/types/transaction.py @@ -0,0 +1,92 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import datetime +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["Transaction", "AdditionalInfo"] + + +class AdditionalInfo(BaseModel): + """Additional transaction-specific fields that vary by source""" + + capital_withdrawal: Optional[float] = None + """Capital withdrawal amount (CDSL MF transactions)""" + + credit: Optional[float] = None + """Units credited (demat transactions)""" + + debit: Optional[float] = None + """Units debited (demat transactions)""" + + income_distribution: Optional[float] = None + """Income distribution amount (CDSL MF transactions)""" + + order_no: Optional[str] = None + """Order/transaction reference number (demat transactions)""" + + price: Optional[float] = None + """Price per unit (NSDL/CDSL MF transactions)""" + + stamp_duty: Optional[float] = None + """Stamp duty charged""" + + +class Transaction(BaseModel): + """ + Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) + """ + + additional_info: Optional[AdditionalInfo] = None + """Additional transaction-specific fields that vary by source""" + + amount: Optional[float] = None + """Transaction amount in currency (computed from units × price/NAV)""" + + balance: Optional[float] = None + """Balance units after transaction""" + + date: Optional[datetime.date] = None + """Transaction date (YYYY-MM-DD)""" + + description: Optional[str] = None + """Transaction description/particulars""" + + dividend_rate: Optional[float] = None + """Dividend rate (for DIVIDEND_PAYOUT transactions)""" + + nav: Optional[float] = None + """NAV/price per unit on transaction date""" + + type: Optional[ + Literal[ + "PURCHASE", + "PURCHASE_SIP", + "REDEMPTION", + "SWITCH_IN", + "SWITCH_IN_MERGER", + "SWITCH_OUT", + "SWITCH_OUT_MERGER", + "DIVIDEND_PAYOUT", + "DIVIDEND_REINVEST", + "SEGREGATION", + "STAMP_DUTY_TAX", + "TDS_TAX", + "STT_TAX", + "MISC", + "REVERSAL", + "UNKNOWN", + ] + ] = None + """Transaction type. + + Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, + SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, + DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, + REVERSAL, UNKNOWN. + """ + + units: Optional[float] = None + """Number of units involved in transaction""" diff --git a/src/cas_parser/types/unified_response.py b/src/cas_parser/types/unified_response.py index 2a8ab94..42a64a7 100644 --- a/src/cas_parser/types/unified_response.py +++ b/src/cas_parser/types/unified_response.py @@ -1,12 +1,14 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import datetime from typing import List, Optional +from datetime import date, datetime from typing_extensions import Literal from pydantic import Field as FieldInfo from .._models import BaseModel +from .transaction import Transaction +from .linked_holder import LinkedHolder __all__ = [ "UnifiedResponse", @@ -15,25 +17,14 @@ "DematAccountHoldings", "DematAccountHoldingsAif", "DematAccountHoldingsAifAdditionalInfo", - "DematAccountHoldingsAifTransaction", - "DematAccountHoldingsAifTransactionAdditionalInfo", "DematAccountHoldingsCorporateBond", "DematAccountHoldingsCorporateBondAdditionalInfo", - "DematAccountHoldingsCorporateBondTransaction", - "DematAccountHoldingsCorporateBondTransactionAdditionalInfo", "DematAccountHoldingsDematMutualFund", "DematAccountHoldingsDematMutualFundAdditionalInfo", - "DematAccountHoldingsDematMutualFundTransaction", - "DematAccountHoldingsDematMutualFundTransactionAdditionalInfo", "DematAccountHoldingsEquity", "DematAccountHoldingsEquityAdditionalInfo", - "DematAccountHoldingsEquityTransaction", - "DematAccountHoldingsEquityTransactionAdditionalInfo", "DematAccountHoldingsGovernmentSecurity", "DematAccountHoldingsGovernmentSecurityAdditionalInfo", - "DematAccountHoldingsGovernmentSecurityTransaction", - "DematAccountHoldingsGovernmentSecurityTransactionAdditionalInfo", - "DematAccountLinkedHolder", "Insurance", "InsuranceLifeInsurancePolicy", "Investor", @@ -41,16 +32,12 @@ "MetaStatementPeriod", "MutualFund", "MutualFundAdditionalInfo", - "MutualFundLinkedHolder", "MutualFundScheme", "MutualFundSchemeAdditionalInfo", "MutualFundSchemeGain", - "MutualFundSchemeTransaction", - "MutualFundSchemeTransactionAdditionalInfo", "Np", "NpFund", "NpFundAdditionalInfo", - "NpLinkedHolder", "Summary", "SummaryAccounts", "SummaryAccountsDemat", @@ -98,89 +85,6 @@ class DematAccountHoldingsAifAdditionalInfo(BaseModel): """Opening balance units for the statement period (beta)""" -class DematAccountHoldingsAifTransactionAdditionalInfo(BaseModel): - """Additional transaction-specific fields that vary by source""" - - capital_withdrawal: Optional[float] = None - """Capital withdrawal amount (CDSL MF transactions)""" - - credit: Optional[float] = None - """Units credited (demat transactions)""" - - debit: Optional[float] = None - """Units debited (demat transactions)""" - - income_distribution: Optional[float] = None - """Income distribution amount (CDSL MF transactions)""" - - order_no: Optional[str] = None - """Order/transaction reference number (demat transactions)""" - - price: Optional[float] = None - """Price per unit (NSDL/CDSL MF transactions)""" - - stamp_duty: Optional[float] = None - """Stamp duty charged""" - - -class DematAccountHoldingsAifTransaction(BaseModel): - """ - Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) - """ - - additional_info: Optional[DematAccountHoldingsAifTransactionAdditionalInfo] = None - """Additional transaction-specific fields that vary by source""" - - amount: Optional[float] = None - """Transaction amount in currency (computed from units × price/NAV)""" - - balance: Optional[float] = None - """Balance units after transaction""" - - date: Optional[datetime.date] = None - """Transaction date (YYYY-MM-DD)""" - - description: Optional[str] = None - """Transaction description/particulars""" - - dividend_rate: Optional[float] = None - """Dividend rate (for DIVIDEND_PAYOUT transactions)""" - - nav: Optional[float] = None - """NAV/price per unit on transaction date""" - - type: Optional[ - Literal[ - "PURCHASE", - "PURCHASE_SIP", - "REDEMPTION", - "SWITCH_IN", - "SWITCH_IN_MERGER", - "SWITCH_OUT", - "SWITCH_OUT_MERGER", - "DIVIDEND_PAYOUT", - "DIVIDEND_REINVEST", - "SEGREGATION", - "STAMP_DUTY_TAX", - "TDS_TAX", - "STT_TAX", - "MISC", - "REVERSAL", - "UNKNOWN", - ] - ] = None - """Transaction type. - - Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, - SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, - DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, - REVERSAL, UNKNOWN. - """ - - units: Optional[float] = None - """Number of units involved in transaction""" - - class DematAccountHoldingsAif(BaseModel): additional_info: Optional[DematAccountHoldingsAifAdditionalInfo] = None """Additional information specific to the AIF""" @@ -191,7 +95,7 @@ class DematAccountHoldingsAif(BaseModel): name: Optional[str] = None """Name of the AIF""" - transactions: Optional[List[DematAccountHoldingsAifTransaction]] = None + transactions: Optional[List[Transaction]] = None """List of transactions for this holding (beta)""" units: Optional[float] = None @@ -211,89 +115,6 @@ class DematAccountHoldingsCorporateBondAdditionalInfo(BaseModel): """Opening balance units for the statement period (beta)""" -class DematAccountHoldingsCorporateBondTransactionAdditionalInfo(BaseModel): - """Additional transaction-specific fields that vary by source""" - - capital_withdrawal: Optional[float] = None - """Capital withdrawal amount (CDSL MF transactions)""" - - credit: Optional[float] = None - """Units credited (demat transactions)""" - - debit: Optional[float] = None - """Units debited (demat transactions)""" - - income_distribution: Optional[float] = None - """Income distribution amount (CDSL MF transactions)""" - - order_no: Optional[str] = None - """Order/transaction reference number (demat transactions)""" - - price: Optional[float] = None - """Price per unit (NSDL/CDSL MF transactions)""" - - stamp_duty: Optional[float] = None - """Stamp duty charged""" - - -class DematAccountHoldingsCorporateBondTransaction(BaseModel): - """ - Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) - """ - - additional_info: Optional[DematAccountHoldingsCorporateBondTransactionAdditionalInfo] = None - """Additional transaction-specific fields that vary by source""" - - amount: Optional[float] = None - """Transaction amount in currency (computed from units × price/NAV)""" - - balance: Optional[float] = None - """Balance units after transaction""" - - date: Optional[datetime.date] = None - """Transaction date (YYYY-MM-DD)""" - - description: Optional[str] = None - """Transaction description/particulars""" - - dividend_rate: Optional[float] = None - """Dividend rate (for DIVIDEND_PAYOUT transactions)""" - - nav: Optional[float] = None - """NAV/price per unit on transaction date""" - - type: Optional[ - Literal[ - "PURCHASE", - "PURCHASE_SIP", - "REDEMPTION", - "SWITCH_IN", - "SWITCH_IN_MERGER", - "SWITCH_OUT", - "SWITCH_OUT_MERGER", - "DIVIDEND_PAYOUT", - "DIVIDEND_REINVEST", - "SEGREGATION", - "STAMP_DUTY_TAX", - "TDS_TAX", - "STT_TAX", - "MISC", - "REVERSAL", - "UNKNOWN", - ] - ] = None - """Transaction type. - - Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, - SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, - DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, - REVERSAL, UNKNOWN. - """ - - units: Optional[float] = None - """Number of units involved in transaction""" - - class DematAccountHoldingsCorporateBond(BaseModel): additional_info: Optional[DematAccountHoldingsCorporateBondAdditionalInfo] = None """Additional information specific to the corporate bond""" @@ -304,7 +125,7 @@ class DematAccountHoldingsCorporateBond(BaseModel): name: Optional[str] = None """Name of the corporate bond""" - transactions: Optional[List[DematAccountHoldingsCorporateBondTransaction]] = None + transactions: Optional[List[Transaction]] = None """List of transactions for this holding (beta)""" units: Optional[float] = None @@ -324,89 +145,6 @@ class DematAccountHoldingsDematMutualFundAdditionalInfo(BaseModel): """Opening balance units for the statement period (beta)""" -class DematAccountHoldingsDematMutualFundTransactionAdditionalInfo(BaseModel): - """Additional transaction-specific fields that vary by source""" - - capital_withdrawal: Optional[float] = None - """Capital withdrawal amount (CDSL MF transactions)""" - - credit: Optional[float] = None - """Units credited (demat transactions)""" - - debit: Optional[float] = None - """Units debited (demat transactions)""" - - income_distribution: Optional[float] = None - """Income distribution amount (CDSL MF transactions)""" - - order_no: Optional[str] = None - """Order/transaction reference number (demat transactions)""" - - price: Optional[float] = None - """Price per unit (NSDL/CDSL MF transactions)""" - - stamp_duty: Optional[float] = None - """Stamp duty charged""" - - -class DematAccountHoldingsDematMutualFundTransaction(BaseModel): - """ - Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) - """ - - additional_info: Optional[DematAccountHoldingsDematMutualFundTransactionAdditionalInfo] = None - """Additional transaction-specific fields that vary by source""" - - amount: Optional[float] = None - """Transaction amount in currency (computed from units × price/NAV)""" - - balance: Optional[float] = None - """Balance units after transaction""" - - date: Optional[datetime.date] = None - """Transaction date (YYYY-MM-DD)""" - - description: Optional[str] = None - """Transaction description/particulars""" - - dividend_rate: Optional[float] = None - """Dividend rate (for DIVIDEND_PAYOUT transactions)""" - - nav: Optional[float] = None - """NAV/price per unit on transaction date""" - - type: Optional[ - Literal[ - "PURCHASE", - "PURCHASE_SIP", - "REDEMPTION", - "SWITCH_IN", - "SWITCH_IN_MERGER", - "SWITCH_OUT", - "SWITCH_OUT_MERGER", - "DIVIDEND_PAYOUT", - "DIVIDEND_REINVEST", - "SEGREGATION", - "STAMP_DUTY_TAX", - "TDS_TAX", - "STT_TAX", - "MISC", - "REVERSAL", - "UNKNOWN", - ] - ] = None - """Transaction type. - - Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, - SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, - DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, - REVERSAL, UNKNOWN. - """ - - units: Optional[float] = None - """Number of units involved in transaction""" - - class DematAccountHoldingsDematMutualFund(BaseModel): additional_info: Optional[DematAccountHoldingsDematMutualFundAdditionalInfo] = None """Additional information specific to the mutual fund""" @@ -417,7 +155,7 @@ class DematAccountHoldingsDematMutualFund(BaseModel): name: Optional[str] = None """Name of the mutual fund""" - transactions: Optional[List[DematAccountHoldingsDematMutualFundTransaction]] = None + transactions: Optional[List[Transaction]] = None """List of transactions for this holding (beta)""" units: Optional[float] = None @@ -437,89 +175,6 @@ class DematAccountHoldingsEquityAdditionalInfo(BaseModel): """Opening balance units for the statement period (beta)""" -class DematAccountHoldingsEquityTransactionAdditionalInfo(BaseModel): - """Additional transaction-specific fields that vary by source""" - - capital_withdrawal: Optional[float] = None - """Capital withdrawal amount (CDSL MF transactions)""" - - credit: Optional[float] = None - """Units credited (demat transactions)""" - - debit: Optional[float] = None - """Units debited (demat transactions)""" - - income_distribution: Optional[float] = None - """Income distribution amount (CDSL MF transactions)""" - - order_no: Optional[str] = None - """Order/transaction reference number (demat transactions)""" - - price: Optional[float] = None - """Price per unit (NSDL/CDSL MF transactions)""" - - stamp_duty: Optional[float] = None - """Stamp duty charged""" - - -class DematAccountHoldingsEquityTransaction(BaseModel): - """ - Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) - """ - - additional_info: Optional[DematAccountHoldingsEquityTransactionAdditionalInfo] = None - """Additional transaction-specific fields that vary by source""" - - amount: Optional[float] = None - """Transaction amount in currency (computed from units × price/NAV)""" - - balance: Optional[float] = None - """Balance units after transaction""" - - date: Optional[datetime.date] = None - """Transaction date (YYYY-MM-DD)""" - - description: Optional[str] = None - """Transaction description/particulars""" - - dividend_rate: Optional[float] = None - """Dividend rate (for DIVIDEND_PAYOUT transactions)""" - - nav: Optional[float] = None - """NAV/price per unit on transaction date""" - - type: Optional[ - Literal[ - "PURCHASE", - "PURCHASE_SIP", - "REDEMPTION", - "SWITCH_IN", - "SWITCH_IN_MERGER", - "SWITCH_OUT", - "SWITCH_OUT_MERGER", - "DIVIDEND_PAYOUT", - "DIVIDEND_REINVEST", - "SEGREGATION", - "STAMP_DUTY_TAX", - "TDS_TAX", - "STT_TAX", - "MISC", - "REVERSAL", - "UNKNOWN", - ] - ] = None - """Transaction type. - - Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, - SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, - DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, - REVERSAL, UNKNOWN. - """ - - units: Optional[float] = None - """Number of units involved in transaction""" - - class DematAccountHoldingsEquity(BaseModel): additional_info: Optional[DematAccountHoldingsEquityAdditionalInfo] = None """Additional information specific to the equity""" @@ -530,7 +185,7 @@ class DematAccountHoldingsEquity(BaseModel): name: Optional[str] = None """Name of the equity""" - transactions: Optional[List[DematAccountHoldingsEquityTransaction]] = None + transactions: Optional[List[Transaction]] = None """List of transactions for this holding (beta)""" units: Optional[float] = None @@ -550,89 +205,6 @@ class DematAccountHoldingsGovernmentSecurityAdditionalInfo(BaseModel): """Opening balance units for the statement period (beta)""" -class DematAccountHoldingsGovernmentSecurityTransactionAdditionalInfo(BaseModel): - """Additional transaction-specific fields that vary by source""" - - capital_withdrawal: Optional[float] = None - """Capital withdrawal amount (CDSL MF transactions)""" - - credit: Optional[float] = None - """Units credited (demat transactions)""" - - debit: Optional[float] = None - """Units debited (demat transactions)""" - - income_distribution: Optional[float] = None - """Income distribution amount (CDSL MF transactions)""" - - order_no: Optional[str] = None - """Order/transaction reference number (demat transactions)""" - - price: Optional[float] = None - """Price per unit (NSDL/CDSL MF transactions)""" - - stamp_duty: Optional[float] = None - """Stamp duty charged""" - - -class DematAccountHoldingsGovernmentSecurityTransaction(BaseModel): - """ - Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) - """ - - additional_info: Optional[DematAccountHoldingsGovernmentSecurityTransactionAdditionalInfo] = None - """Additional transaction-specific fields that vary by source""" - - amount: Optional[float] = None - """Transaction amount in currency (computed from units × price/NAV)""" - - balance: Optional[float] = None - """Balance units after transaction""" - - date: Optional[datetime.date] = None - """Transaction date (YYYY-MM-DD)""" - - description: Optional[str] = None - """Transaction description/particulars""" - - dividend_rate: Optional[float] = None - """Dividend rate (for DIVIDEND_PAYOUT transactions)""" - - nav: Optional[float] = None - """NAV/price per unit on transaction date""" - - type: Optional[ - Literal[ - "PURCHASE", - "PURCHASE_SIP", - "REDEMPTION", - "SWITCH_IN", - "SWITCH_IN_MERGER", - "SWITCH_OUT", - "SWITCH_OUT_MERGER", - "DIVIDEND_PAYOUT", - "DIVIDEND_REINVEST", - "SEGREGATION", - "STAMP_DUTY_TAX", - "TDS_TAX", - "STT_TAX", - "MISC", - "REVERSAL", - "UNKNOWN", - ] - ] = None - """Transaction type. - - Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, - SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, - DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, - REVERSAL, UNKNOWN. - """ - - units: Optional[float] = None - """Number of units involved in transaction""" - - class DematAccountHoldingsGovernmentSecurity(BaseModel): additional_info: Optional[DematAccountHoldingsGovernmentSecurityAdditionalInfo] = None """Additional information specific to the government security""" @@ -643,7 +215,7 @@ class DematAccountHoldingsGovernmentSecurity(BaseModel): name: Optional[str] = None """Name of the government security""" - transactions: Optional[List[DematAccountHoldingsGovernmentSecurityTransaction]] = None + transactions: Optional[List[Transaction]] = None """List of transactions for this holding (beta)""" units: Optional[float] = None @@ -665,14 +237,6 @@ class DematAccountHoldings(BaseModel): government_securities: Optional[List[DematAccountHoldingsGovernmentSecurity]] = None -class DematAccountLinkedHolder(BaseModel): - name: Optional[str] = None - """Name of the account holder""" - - pan: Optional[str] = None - """PAN of the account holder""" - - class DematAccount(BaseModel): additional_info: Optional[DematAccountAdditionalInfo] = None """Additional information specific to the demat account type""" @@ -694,7 +258,7 @@ class DematAccount(BaseModel): holdings: Optional[DematAccountHoldings] = None - linked_holders: Optional[List[DematAccountLinkedHolder]] = None + linked_holders: Optional[List[LinkedHolder]] = None """List of account holders linked to this demat account""" value: Optional[float] = None @@ -758,10 +322,10 @@ class Investor(BaseModel): class MetaStatementPeriod(BaseModel): - from_: Optional[datetime.date] = FieldInfo(alias="from", default=None) + from_: Optional[date] = FieldInfo(alias="from", default=None) """Start date of the statement period""" - to: Optional[datetime.date] = None + to: Optional[date] = None """End date of the statement period""" @@ -769,7 +333,7 @@ class Meta(BaseModel): cas_type: Optional[Literal["NSDL", "CDSL", "CAMS_KFINTECH"]] = None """Type of CAS detected and processed""" - generated_at: Optional[datetime.datetime] = None + generated_at: Optional[datetime] = None """Timestamp when the response was generated""" statement_period: Optional[MetaStatementPeriod] = None @@ -788,14 +352,6 @@ class MutualFundAdditionalInfo(BaseModel): """PAN KYC status""" -class MutualFundLinkedHolder(BaseModel): - name: Optional[str] = None - """Name of the account holder""" - - pan: Optional[str] = None - """PAN of the account holder""" - - class MutualFundSchemeAdditionalInfo(BaseModel): """Additional information specific to the scheme""" @@ -823,89 +379,6 @@ class MutualFundSchemeGain(BaseModel): """Percentage gain or loss""" -class MutualFundSchemeTransactionAdditionalInfo(BaseModel): - """Additional transaction-specific fields that vary by source""" - - capital_withdrawal: Optional[float] = None - """Capital withdrawal amount (CDSL MF transactions)""" - - credit: Optional[float] = None - """Units credited (demat transactions)""" - - debit: Optional[float] = None - """Units debited (demat transactions)""" - - income_distribution: Optional[float] = None - """Income distribution amount (CDSL MF transactions)""" - - order_no: Optional[str] = None - """Order/transaction reference number (demat transactions)""" - - price: Optional[float] = None - """Price per unit (NSDL/CDSL MF transactions)""" - - stamp_duty: Optional[float] = None - """Stamp duty charged""" - - -class MutualFundSchemeTransaction(BaseModel): - """ - Unified transaction schema for all holding types (MF folios, equities, bonds, etc.) - """ - - additional_info: Optional[MutualFundSchemeTransactionAdditionalInfo] = None - """Additional transaction-specific fields that vary by source""" - - amount: Optional[float] = None - """Transaction amount in currency (computed from units × price/NAV)""" - - balance: Optional[float] = None - """Balance units after transaction""" - - date: Optional[datetime.date] = None - """Transaction date (YYYY-MM-DD)""" - - description: Optional[str] = None - """Transaction description/particulars""" - - dividend_rate: Optional[float] = None - """Dividend rate (for DIVIDEND_PAYOUT transactions)""" - - nav: Optional[float] = None - """NAV/price per unit on transaction date""" - - type: Optional[ - Literal[ - "PURCHASE", - "PURCHASE_SIP", - "REDEMPTION", - "SWITCH_IN", - "SWITCH_IN_MERGER", - "SWITCH_OUT", - "SWITCH_OUT_MERGER", - "DIVIDEND_PAYOUT", - "DIVIDEND_REINVEST", - "SEGREGATION", - "STAMP_DUTY_TAX", - "TDS_TAX", - "STT_TAX", - "MISC", - "REVERSAL", - "UNKNOWN", - ] - ] = None - """Transaction type. - - Possible values are PURCHASE, PURCHASE_SIP, REDEMPTION, SWITCH_IN, - SWITCH_IN_MERGER, SWITCH_OUT, SWITCH_OUT_MERGER, DIVIDEND_PAYOUT, - DIVIDEND_REINVEST, SEGREGATION, STAMP_DUTY_TAX, TDS_TAX, STT_TAX, MISC, - REVERSAL, UNKNOWN. - """ - - units: Optional[float] = None - """Number of units involved in transaction""" - - class MutualFundScheme(BaseModel): additional_info: Optional[MutualFundSchemeAdditionalInfo] = None """Additional information specific to the scheme""" @@ -927,7 +400,7 @@ class MutualFundScheme(BaseModel): nominees: Optional[List[str]] = None """List of nominees""" - transactions: Optional[List[MutualFundSchemeTransaction]] = None + transactions: Optional[List[Transaction]] = None type: Optional[Literal["Equity", "Debt", "Hybrid", "Other"]] = None """Type of mutual fund scheme""" @@ -949,7 +422,7 @@ class MutualFund(BaseModel): folio_number: Optional[str] = None """Folio number""" - linked_holders: Optional[List[MutualFundLinkedHolder]] = None + linked_holders: Optional[List[LinkedHolder]] = None """List of account holders linked to this mutual fund folio""" registrar: Optional[str] = None @@ -991,14 +464,6 @@ class NpFund(BaseModel): """Current market value of the holding""" -class NpLinkedHolder(BaseModel): - name: Optional[str] = None - """Name of the account holder""" - - pan: Optional[str] = None - """PAN of the account holder""" - - class Np(BaseModel): additional_info: Optional[object] = None """Additional information specific to the NPS account""" @@ -1008,7 +473,7 @@ class Np(BaseModel): funds: Optional[List[NpFund]] = None - linked_holders: Optional[List[NpLinkedHolder]] = None + linked_holders: Optional[List[LinkedHolder]] = None """List of account holders linked to this NPS account""" pran: Optional[str] = None diff --git a/src/cas_parser/types/verify_token_verify_response.py b/src/cas_parser/types/verify_token_verify_response.py new file mode 100644 index 0000000..fcfebe7 --- /dev/null +++ b/src/cas_parser/types/verify_token_verify_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["VerifyTokenVerifyResponse"] + + +class VerifyTokenVerifyResponse(BaseModel): + error: Optional[str] = None + """Error message (only shown if invalid)""" + + masked_api_key: Optional[str] = None + """Masked API key (only shown if valid)""" + + valid: Optional[bool] = None + """Whether the token is valid""" diff --git a/tests/api_resources/cdsl/__init__.py b/tests/api_resources/cdsl/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/cdsl/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/cdsl/test_fetch.py b/tests/api_resources/cdsl/test_fetch.py new file mode 100644 index 0000000..a4b92cc --- /dev/null +++ b/tests/api_resources/cdsl/test_fetch.py @@ -0,0 +1,219 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types.cdsl import ( + FetchVerifyOtpResponse, + FetchRequestOtpResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestFetch: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_request_otp(self, client: CasParser) -> None: + fetch = client.cdsl.fetch.request_otp( + bo_id="1234567890123456", + dob="1990-01-15", + pan="ABCDE1234F", + ) + assert_matches_type(FetchRequestOtpResponse, fetch, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_request_otp(self, client: CasParser) -> None: + response = client.cdsl.fetch.with_raw_response.request_otp( + bo_id="1234567890123456", + dob="1990-01-15", + pan="ABCDE1234F", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + fetch = response.parse() + assert_matches_type(FetchRequestOtpResponse, fetch, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_request_otp(self, client: CasParser) -> None: + with client.cdsl.fetch.with_streaming_response.request_otp( + bo_id="1234567890123456", + dob="1990-01-15", + pan="ABCDE1234F", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + fetch = response.parse() + assert_matches_type(FetchRequestOtpResponse, fetch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_verify_otp(self, client: CasParser) -> None: + fetch = client.cdsl.fetch.verify_otp( + session_id="session_id", + otp="123456", + ) + assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_verify_otp_with_all_params(self, client: CasParser) -> None: + fetch = client.cdsl.fetch.verify_otp( + session_id="session_id", + otp="123456", + num_periods=6, + ) + assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_verify_otp(self, client: CasParser) -> None: + response = client.cdsl.fetch.with_raw_response.verify_otp( + session_id="session_id", + otp="123456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + fetch = response.parse() + assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_verify_otp(self, client: CasParser) -> None: + with client.cdsl.fetch.with_streaming_response.verify_otp( + session_id="session_id", + otp="123456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + fetch = response.parse() + assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_verify_otp(self, client: CasParser) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + client.cdsl.fetch.with_raw_response.verify_otp( + session_id="", + otp="123456", + ) + + +class TestAsyncFetch: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_request_otp(self, async_client: AsyncCasParser) -> None: + fetch = await async_client.cdsl.fetch.request_otp( + bo_id="1234567890123456", + dob="1990-01-15", + pan="ABCDE1234F", + ) + assert_matches_type(FetchRequestOtpResponse, fetch, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_request_otp(self, async_client: AsyncCasParser) -> None: + response = await async_client.cdsl.fetch.with_raw_response.request_otp( + bo_id="1234567890123456", + dob="1990-01-15", + pan="ABCDE1234F", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + fetch = await response.parse() + assert_matches_type(FetchRequestOtpResponse, fetch, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_request_otp(self, async_client: AsyncCasParser) -> None: + async with async_client.cdsl.fetch.with_streaming_response.request_otp( + bo_id="1234567890123456", + dob="1990-01-15", + pan="ABCDE1234F", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + fetch = await response.parse() + assert_matches_type(FetchRequestOtpResponse, fetch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_verify_otp(self, async_client: AsyncCasParser) -> None: + fetch = await async_client.cdsl.fetch.verify_otp( + session_id="session_id", + otp="123456", + ) + assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_verify_otp_with_all_params(self, async_client: AsyncCasParser) -> None: + fetch = await async_client.cdsl.fetch.verify_otp( + session_id="session_id", + otp="123456", + num_periods=6, + ) + assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_verify_otp(self, async_client: AsyncCasParser) -> None: + response = await async_client.cdsl.fetch.with_raw_response.verify_otp( + session_id="session_id", + otp="123456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + fetch = await response.parse() + assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_verify_otp(self, async_client: AsyncCasParser) -> None: + async with async_client.cdsl.fetch.with_streaming_response.verify_otp( + session_id="session_id", + otp="123456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + fetch = await response.parse() + assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_verify_otp(self, async_client: AsyncCasParser) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + await async_client.cdsl.fetch.with_raw_response.verify_otp( + session_id="", + otp="123456", + ) diff --git a/tests/api_resources/test_access_token.py b/tests/api_resources/test_access_token.py new file mode 100644 index 0000000..3edd508 --- /dev/null +++ b/tests/api_resources/test_access_token.py @@ -0,0 +1,96 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import AccessTokenCreateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAccessToken: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create(self, client: CasParser) -> None: + access_token = client.access_token.create() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: CasParser) -> None: + access_token = client.access_token.create( + expiry_minutes=60, + ) + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: CasParser) -> None: + response = client.access_token.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + access_token = response.parse() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: CasParser) -> None: + with client.access_token.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + access_token = response.parse() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncAccessToken: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncCasParser) -> None: + access_token = await async_client.access_token.create() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncCasParser) -> None: + access_token = await async_client.access_token.create( + expiry_minutes=60, + ) + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: + response = await async_client.access_token.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + access_token = await response.parse() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncCasParser) -> None: + async with async_client.access_token.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + access_token = await response.parse() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_cams_kfintech.py b/tests/api_resources/test_cams_kfintech.py new file mode 100644 index 0000000..b9d8d0e --- /dev/null +++ b/tests/api_resources/test_cams_kfintech.py @@ -0,0 +1,100 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import UnifiedResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCamsKfintech: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_parse(self, client: CasParser) -> None: + cams_kfintech = client.cams_kfintech.parse() + assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_parse_with_all_params(self, client: CasParser) -> None: + cams_kfintech = client.cams_kfintech.parse( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_parse(self, client: CasParser) -> None: + response = client.cams_kfintech.with_raw_response.parse() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cams_kfintech = response.parse() + assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_parse(self, client: CasParser) -> None: + with client.cams_kfintech.with_streaming_response.parse() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cams_kfintech = response.parse() + assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncCamsKfintech: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_parse(self, async_client: AsyncCasParser) -> None: + cams_kfintech = await async_client.cams_kfintech.parse() + assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_parse_with_all_params(self, async_client: AsyncCasParser) -> None: + cams_kfintech = await async_client.cams_kfintech.parse( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_parse(self, async_client: AsyncCasParser) -> None: + response = await async_client.cams_kfintech.with_raw_response.parse() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cams_kfintech = await response.parse() + assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_parse(self, async_client: AsyncCasParser) -> None: + async with async_client.cams_kfintech.with_streaming_response.parse() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cams_kfintech = await response.parse() + assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_cas_parser.py b/tests/api_resources/test_cas_parser.py deleted file mode 100644 index cf0509b..0000000 --- a/tests/api_resources/test_cas_parser.py +++ /dev/null @@ -1,330 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from cas_parser import CasParser, AsyncCasParser -from tests.utils import assert_matches_type -from cas_parser.types import ( - UnifiedResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestCasParser: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_cams_kfintech(self, client: CasParser) -> None: - cas_parser = client.cas_parser.cams_kfintech() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_cams_kfintech_with_all_params(self, client: CasParser) -> None: - cas_parser = client.cas_parser.cams_kfintech( - password="password", - pdf_file="pdf_file", - pdf_url="https://example.com", - ) - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_cams_kfintech(self, client: CasParser) -> None: - response = client.cas_parser.with_raw_response.cams_kfintech() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - cas_parser = response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_cams_kfintech(self, client: CasParser) -> None: - with client.cas_parser.with_streaming_response.cams_kfintech() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - cas_parser = response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_cdsl(self, client: CasParser) -> None: - cas_parser = client.cas_parser.cdsl() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_cdsl_with_all_params(self, client: CasParser) -> None: - cas_parser = client.cas_parser.cdsl( - password="password", - pdf_file="pdf_file", - pdf_url="https://example.com", - ) - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_cdsl(self, client: CasParser) -> None: - response = client.cas_parser.with_raw_response.cdsl() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - cas_parser = response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_cdsl(self, client: CasParser) -> None: - with client.cas_parser.with_streaming_response.cdsl() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - cas_parser = response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_nsdl(self, client: CasParser) -> None: - cas_parser = client.cas_parser.nsdl() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_nsdl_with_all_params(self, client: CasParser) -> None: - cas_parser = client.cas_parser.nsdl( - password="password", - pdf_file="pdf_file", - pdf_url="https://example.com", - ) - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_nsdl(self, client: CasParser) -> None: - response = client.cas_parser.with_raw_response.nsdl() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - cas_parser = response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_nsdl(self, client: CasParser) -> None: - with client.cas_parser.with_streaming_response.nsdl() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - cas_parser = response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_smart_parse(self, client: CasParser) -> None: - cas_parser = client.cas_parser.smart_parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_smart_parse_with_all_params(self, client: CasParser) -> None: - cas_parser = client.cas_parser.smart_parse( - password="password", - pdf_file="pdf_file", - pdf_url="https://example.com", - ) - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_smart_parse(self, client: CasParser) -> None: - response = client.cas_parser.with_raw_response.smart_parse() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - cas_parser = response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_smart_parse(self, client: CasParser) -> None: - with client.cas_parser.with_streaming_response.smart_parse() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - cas_parser = response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncCasParser: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_cams_kfintech(self, async_client: AsyncCasParser) -> None: - cas_parser = await async_client.cas_parser.cams_kfintech() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_cams_kfintech_with_all_params(self, async_client: AsyncCasParser) -> None: - cas_parser = await async_client.cas_parser.cams_kfintech( - password="password", - pdf_file="pdf_file", - pdf_url="https://example.com", - ) - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_cams_kfintech(self, async_client: AsyncCasParser) -> None: - response = await async_client.cas_parser.with_raw_response.cams_kfintech() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - cas_parser = await response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_cams_kfintech(self, async_client: AsyncCasParser) -> None: - async with async_client.cas_parser.with_streaming_response.cams_kfintech() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - cas_parser = await response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_cdsl(self, async_client: AsyncCasParser) -> None: - cas_parser = await async_client.cas_parser.cdsl() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_cdsl_with_all_params(self, async_client: AsyncCasParser) -> None: - cas_parser = await async_client.cas_parser.cdsl( - password="password", - pdf_file="pdf_file", - pdf_url="https://example.com", - ) - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_cdsl(self, async_client: AsyncCasParser) -> None: - response = await async_client.cas_parser.with_raw_response.cdsl() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - cas_parser = await response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_cdsl(self, async_client: AsyncCasParser) -> None: - async with async_client.cas_parser.with_streaming_response.cdsl() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - cas_parser = await response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_nsdl(self, async_client: AsyncCasParser) -> None: - cas_parser = await async_client.cas_parser.nsdl() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_nsdl_with_all_params(self, async_client: AsyncCasParser) -> None: - cas_parser = await async_client.cas_parser.nsdl( - password="password", - pdf_file="pdf_file", - pdf_url="https://example.com", - ) - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_nsdl(self, async_client: AsyncCasParser) -> None: - response = await async_client.cas_parser.with_raw_response.nsdl() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - cas_parser = await response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_nsdl(self, async_client: AsyncCasParser) -> None: - async with async_client.cas_parser.with_streaming_response.nsdl() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - cas_parser = await response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_smart_parse(self, async_client: AsyncCasParser) -> None: - cas_parser = await async_client.cas_parser.smart_parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_smart_parse_with_all_params(self, async_client: AsyncCasParser) -> None: - cas_parser = await async_client.cas_parser.smart_parse( - password="password", - pdf_file="pdf_file", - pdf_url="https://example.com", - ) - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_smart_parse(self, async_client: AsyncCasParser) -> None: - response = await async_client.cas_parser.with_raw_response.smart_parse() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - cas_parser = await response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_smart_parse(self, async_client: AsyncCasParser) -> None: - async with async_client.cas_parser.with_streaming_response.smart_parse() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - cas_parser = await response.parse() - assert_matches_type(UnifiedResponse, cas_parser, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_cdsl.py b/tests/api_resources/test_cdsl.py new file mode 100644 index 0000000..a047f79 --- /dev/null +++ b/tests/api_resources/test_cdsl.py @@ -0,0 +1,100 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import UnifiedResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCdsl: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_parse_pdf(self, client: CasParser) -> None: + cdsl = client.cdsl.parse_pdf() + assert_matches_type(UnifiedResponse, cdsl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_parse_pdf_with_all_params(self, client: CasParser) -> None: + cdsl = client.cdsl.parse_pdf( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cdsl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_parse_pdf(self, client: CasParser) -> None: + response = client.cdsl.with_raw_response.parse_pdf() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cdsl = response.parse() + assert_matches_type(UnifiedResponse, cdsl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_parse_pdf(self, client: CasParser) -> None: + with client.cdsl.with_streaming_response.parse_pdf() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cdsl = response.parse() + assert_matches_type(UnifiedResponse, cdsl, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncCdsl: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_parse_pdf(self, async_client: AsyncCasParser) -> None: + cdsl = await async_client.cdsl.parse_pdf() + assert_matches_type(UnifiedResponse, cdsl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_parse_pdf_with_all_params(self, async_client: AsyncCasParser) -> None: + cdsl = await async_client.cdsl.parse_pdf( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, cdsl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_parse_pdf(self, async_client: AsyncCasParser) -> None: + response = await async_client.cdsl.with_raw_response.parse_pdf() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + cdsl = await response.parse() + assert_matches_type(UnifiedResponse, cdsl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_parse_pdf(self, async_client: AsyncCasParser) -> None: + async with async_client.cdsl.with_streaming_response.parse_pdf() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + cdsl = await response.parse() + assert_matches_type(UnifiedResponse, cdsl, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_contract_note.py b/tests/api_resources/test_contract_note.py new file mode 100644 index 0000000..5e39156 --- /dev/null +++ b/tests/api_resources/test_contract_note.py @@ -0,0 +1,102 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import ContractNoteParseResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestContractNote: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_parse(self, client: CasParser) -> None: + contract_note = client.contract_note.parse() + assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_parse_with_all_params(self, client: CasParser) -> None: + contract_note = client.contract_note.parse( + broker_type="zerodha", + password="FAXAK2545F", + pdf_file="JVBERi0xLjQKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwo...", + pdf_url="https://example.com/contract_note.pdf", + ) + assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_parse(self, client: CasParser) -> None: + response = client.contract_note.with_raw_response.parse() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contract_note = response.parse() + assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_parse(self, client: CasParser) -> None: + with client.contract_note.with_streaming_response.parse() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contract_note = response.parse() + assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncContractNote: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_parse(self, async_client: AsyncCasParser) -> None: + contract_note = await async_client.contract_note.parse() + assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_parse_with_all_params(self, async_client: AsyncCasParser) -> None: + contract_note = await async_client.contract_note.parse( + broker_type="zerodha", + password="FAXAK2545F", + pdf_file="JVBERi0xLjQKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwo...", + pdf_url="https://example.com/contract_note.pdf", + ) + assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_parse(self, async_client: AsyncCasParser) -> None: + response = await async_client.contract_note.with_raw_response.parse() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contract_note = await response.parse() + assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_parse(self, async_client: AsyncCasParser) -> None: + async with async_client.contract_note.with_streaming_response.parse() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contract_note = await response.parse() + assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_credits.py b/tests/api_resources/test_credits.py new file mode 100644 index 0000000..e761b62 --- /dev/null +++ b/tests/api_resources/test_credits.py @@ -0,0 +1,80 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import CreditCheckResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCredits: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_check(self, client: CasParser) -> None: + credit = client.credits.check() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_check(self, client: CasParser) -> None: + response = client.credits.with_raw_response.check() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credit = response.parse() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_check(self, client: CasParser) -> None: + with client.credits.with_streaming_response.check() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credit = response.parse() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncCredits: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_check(self, async_client: AsyncCasParser) -> None: + credit = await async_client.credits.check() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_check(self, async_client: AsyncCasParser) -> None: + response = await async_client.credits.with_raw_response.check() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credit = await response.parse() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_check(self, async_client: AsyncCasParser) -> None: + async with async_client.credits.with_streaming_response.check() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credit = await response.parse() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_inbox.py b/tests/api_resources/test_inbox.py new file mode 100644 index 0000000..d41ebc8 --- /dev/null +++ b/tests/api_resources/test_inbox.py @@ -0,0 +1,342 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import ( + InboxConnectEmailResponse, + InboxListCasFilesResponse, + InboxDisconnectEmailResponse, + InboxCheckConnectionStatusResponse, +) +from cas_parser._utils import parse_date + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestInbox: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_check_connection_status(self, client: CasParser) -> None: + inbox = client.inbox.check_connection_status( + x_inbox_token="x-inbox-token", + ) + assert_matches_type(InboxCheckConnectionStatusResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_check_connection_status(self, client: CasParser) -> None: + response = client.inbox.with_raw_response.check_connection_status( + x_inbox_token="x-inbox-token", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbox = response.parse() + assert_matches_type(InboxCheckConnectionStatusResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_check_connection_status(self, client: CasParser) -> None: + with client.inbox.with_streaming_response.check_connection_status( + x_inbox_token="x-inbox-token", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbox = response.parse() + assert_matches_type(InboxCheckConnectionStatusResponse, inbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_connect_email(self, client: CasParser) -> None: + inbox = client.inbox.connect_email( + redirect_uri="https://yourapp.com/oauth-callback", + ) + assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_connect_email_with_all_params(self, client: CasParser) -> None: + inbox = client.inbox.connect_email( + redirect_uri="https://yourapp.com/oauth-callback", + state="abc123", + ) + assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_connect_email(self, client: CasParser) -> None: + response = client.inbox.with_raw_response.connect_email( + redirect_uri="https://yourapp.com/oauth-callback", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbox = response.parse() + assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_connect_email(self, client: CasParser) -> None: + with client.inbox.with_streaming_response.connect_email( + redirect_uri="https://yourapp.com/oauth-callback", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbox = response.parse() + assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_disconnect_email(self, client: CasParser) -> None: + inbox = client.inbox.disconnect_email( + x_inbox_token="x-inbox-token", + ) + assert_matches_type(InboxDisconnectEmailResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_disconnect_email(self, client: CasParser) -> None: + response = client.inbox.with_raw_response.disconnect_email( + x_inbox_token="x-inbox-token", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbox = response.parse() + assert_matches_type(InboxDisconnectEmailResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_disconnect_email(self, client: CasParser) -> None: + with client.inbox.with_streaming_response.disconnect_email( + x_inbox_token="x-inbox-token", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbox = response.parse() + assert_matches_type(InboxDisconnectEmailResponse, inbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list_cas_files(self, client: CasParser) -> None: + inbox = client.inbox.list_cas_files( + x_inbox_token="x-inbox-token", + ) + assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list_cas_files_with_all_params(self, client: CasParser) -> None: + inbox = client.inbox.list_cas_files( + x_inbox_token="x-inbox-token", + cas_types=["cdsl", "nsdl"], + end_date=parse_date("2025-12-31"), + start_date=parse_date("2025-12-01"), + ) + assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list_cas_files(self, client: CasParser) -> None: + response = client.inbox.with_raw_response.list_cas_files( + x_inbox_token="x-inbox-token", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbox = response.parse() + assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list_cas_files(self, client: CasParser) -> None: + with client.inbox.with_streaming_response.list_cas_files( + x_inbox_token="x-inbox-token", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbox = response.parse() + assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncInbox: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_check_connection_status(self, async_client: AsyncCasParser) -> None: + inbox = await async_client.inbox.check_connection_status( + x_inbox_token="x-inbox-token", + ) + assert_matches_type(InboxCheckConnectionStatusResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_check_connection_status(self, async_client: AsyncCasParser) -> None: + response = await async_client.inbox.with_raw_response.check_connection_status( + x_inbox_token="x-inbox-token", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbox = await response.parse() + assert_matches_type(InboxCheckConnectionStatusResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_check_connection_status(self, async_client: AsyncCasParser) -> None: + async with async_client.inbox.with_streaming_response.check_connection_status( + x_inbox_token="x-inbox-token", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbox = await response.parse() + assert_matches_type(InboxCheckConnectionStatusResponse, inbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_connect_email(self, async_client: AsyncCasParser) -> None: + inbox = await async_client.inbox.connect_email( + redirect_uri="https://yourapp.com/oauth-callback", + ) + assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_connect_email_with_all_params(self, async_client: AsyncCasParser) -> None: + inbox = await async_client.inbox.connect_email( + redirect_uri="https://yourapp.com/oauth-callback", + state="abc123", + ) + assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_connect_email(self, async_client: AsyncCasParser) -> None: + response = await async_client.inbox.with_raw_response.connect_email( + redirect_uri="https://yourapp.com/oauth-callback", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbox = await response.parse() + assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_connect_email(self, async_client: AsyncCasParser) -> None: + async with async_client.inbox.with_streaming_response.connect_email( + redirect_uri="https://yourapp.com/oauth-callback", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbox = await response.parse() + assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_disconnect_email(self, async_client: AsyncCasParser) -> None: + inbox = await async_client.inbox.disconnect_email( + x_inbox_token="x-inbox-token", + ) + assert_matches_type(InboxDisconnectEmailResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_disconnect_email(self, async_client: AsyncCasParser) -> None: + response = await async_client.inbox.with_raw_response.disconnect_email( + x_inbox_token="x-inbox-token", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbox = await response.parse() + assert_matches_type(InboxDisconnectEmailResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_disconnect_email(self, async_client: AsyncCasParser) -> None: + async with async_client.inbox.with_streaming_response.disconnect_email( + x_inbox_token="x-inbox-token", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbox = await response.parse() + assert_matches_type(InboxDisconnectEmailResponse, inbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list_cas_files(self, async_client: AsyncCasParser) -> None: + inbox = await async_client.inbox.list_cas_files( + x_inbox_token="x-inbox-token", + ) + assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list_cas_files_with_all_params(self, async_client: AsyncCasParser) -> None: + inbox = await async_client.inbox.list_cas_files( + x_inbox_token="x-inbox-token", + cas_types=["cdsl", "nsdl"], + end_date=parse_date("2025-12-31"), + start_date=parse_date("2025-12-01"), + ) + assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list_cas_files(self, async_client: AsyncCasParser) -> None: + response = await async_client.inbox.with_raw_response.list_cas_files( + x_inbox_token="x-inbox-token", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbox = await response.parse() + assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list_cas_files(self, async_client: AsyncCasParser) -> None: + async with async_client.inbox.with_streaming_response.list_cas_files( + x_inbox_token="x-inbox-token", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbox = await response.parse() + assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_kfintech.py b/tests/api_resources/test_kfintech.py new file mode 100644 index 0000000..fee4673 --- /dev/null +++ b/tests/api_resources/test_kfintech.py @@ -0,0 +1,134 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import KfintechGenerateCasResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestKfintech: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_generate_cas(self, client: CasParser) -> None: + kfintech = client.kfintech.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) + assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_generate_cas_with_all_params(self, client: CasParser) -> None: + kfintech = client.kfintech.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + pan_no="ABCDE1234F", + ) + assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_generate_cas(self, client: CasParser) -> None: + response = client.kfintech.with_raw_response.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + kfintech = response.parse() + assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_generate_cas(self, client: CasParser) -> None: + with client.kfintech.with_streaming_response.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + kfintech = response.parse() + assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncKfintech: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_generate_cas(self, async_client: AsyncCasParser) -> None: + kfintech = await async_client.kfintech.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) + assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_generate_cas_with_all_params(self, async_client: AsyncCasParser) -> None: + kfintech = await async_client.kfintech.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + pan_no="ABCDE1234F", + ) + assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_generate_cas(self, async_client: AsyncCasParser) -> None: + response = await async_client.kfintech.with_raw_response.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + kfintech = await response.parse() + assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_generate_cas(self, async_client: AsyncCasParser) -> None: + async with async_client.kfintech.with_streaming_response.generate_cas( + email="user@example.com", + from_date="2023-01-01", + password="Abcdefghi12$", + to_date="2023-12-31", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + kfintech = await response.parse() + assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_logs.py b/tests/api_resources/test_logs.py new file mode 100644 index 0000000..226ca42 --- /dev/null +++ b/tests/api_resources/test_logs.py @@ -0,0 +1,175 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import LogCreateResponse, LogGetSummaryResponse +from cas_parser._utils import parse_datetime + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestLogs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create(self, client: CasParser) -> None: + log = client.logs.create() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: CasParser) -> None: + log = client.logs.create( + end_time=parse_datetime("2026-01-31T23:59:59Z"), + limit=1, + start_time=parse_datetime("2026-01-01T00:00:00Z"), + ) + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: CasParser) -> None: + response = client.logs.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = response.parse() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: CasParser) -> None: + with client.logs.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = response.parse() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_get_summary(self, client: CasParser) -> None: + log = client.logs.get_summary() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_get_summary_with_all_params(self, client: CasParser) -> None: + log = client.logs.get_summary( + end_time=parse_datetime("2019-12-27T18:11:19.117Z"), + start_time=parse_datetime("2019-12-27T18:11:19.117Z"), + ) + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_get_summary(self, client: CasParser) -> None: + response = client.logs.with_raw_response.get_summary() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = response.parse() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_get_summary(self, client: CasParser) -> None: + with client.logs.with_streaming_response.get_summary() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = response.parse() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncLogs: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncCasParser) -> None: + log = await async_client.logs.create() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncCasParser) -> None: + log = await async_client.logs.create( + end_time=parse_datetime("2026-01-31T23:59:59Z"), + limit=1, + start_time=parse_datetime("2026-01-01T00:00:00Z"), + ) + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: + response = await async_client.logs.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = await response.parse() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncCasParser) -> None: + async with async_client.logs.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = await response.parse() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_get_summary(self, async_client: AsyncCasParser) -> None: + log = await async_client.logs.get_summary() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_get_summary_with_all_params(self, async_client: AsyncCasParser) -> None: + log = await async_client.logs.get_summary( + end_time=parse_datetime("2019-12-27T18:11:19.117Z"), + start_time=parse_datetime("2019-12-27T18:11:19.117Z"), + ) + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_get_summary(self, async_client: AsyncCasParser) -> None: + response = await async_client.logs.with_raw_response.get_summary() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = await response.parse() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_get_summary(self, async_client: AsyncCasParser) -> None: + async with async_client.logs.with_streaming_response.get_summary() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = await response.parse() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_nsdl.py b/tests/api_resources/test_nsdl.py new file mode 100644 index 0000000..6e30980 --- /dev/null +++ b/tests/api_resources/test_nsdl.py @@ -0,0 +1,100 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import UnifiedResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestNsdl: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_parse(self, client: CasParser) -> None: + nsdl = client.nsdl.parse() + assert_matches_type(UnifiedResponse, nsdl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_parse_with_all_params(self, client: CasParser) -> None: + nsdl = client.nsdl.parse( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, nsdl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_parse(self, client: CasParser) -> None: + response = client.nsdl.with_raw_response.parse() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + nsdl = response.parse() + assert_matches_type(UnifiedResponse, nsdl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_parse(self, client: CasParser) -> None: + with client.nsdl.with_streaming_response.parse() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + nsdl = response.parse() + assert_matches_type(UnifiedResponse, nsdl, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncNsdl: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_parse(self, async_client: AsyncCasParser) -> None: + nsdl = await async_client.nsdl.parse() + assert_matches_type(UnifiedResponse, nsdl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_parse_with_all_params(self, async_client: AsyncCasParser) -> None: + nsdl = await async_client.nsdl.parse( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, nsdl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_parse(self, async_client: AsyncCasParser) -> None: + response = await async_client.nsdl.with_raw_response.parse() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + nsdl = await response.parse() + assert_matches_type(UnifiedResponse, nsdl, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_parse(self, async_client: AsyncCasParser) -> None: + async with async_client.nsdl.with_streaming_response.parse() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + nsdl = await response.parse() + assert_matches_type(UnifiedResponse, nsdl, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_smart.py b/tests/api_resources/test_smart.py new file mode 100644 index 0000000..152457b --- /dev/null +++ b/tests/api_resources/test_smart.py @@ -0,0 +1,100 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import UnifiedResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestSmart: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_parse_cas_pdf(self, client: CasParser) -> None: + smart = client.smart.parse_cas_pdf() + assert_matches_type(UnifiedResponse, smart, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_parse_cas_pdf_with_all_params(self, client: CasParser) -> None: + smart = client.smart.parse_cas_pdf( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, smart, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_parse_cas_pdf(self, client: CasParser) -> None: + response = client.smart.with_raw_response.parse_cas_pdf() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + smart = response.parse() + assert_matches_type(UnifiedResponse, smart, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_parse_cas_pdf(self, client: CasParser) -> None: + with client.smart.with_streaming_response.parse_cas_pdf() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + smart = response.parse() + assert_matches_type(UnifiedResponse, smart, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncSmart: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_parse_cas_pdf(self, async_client: AsyncCasParser) -> None: + smart = await async_client.smart.parse_cas_pdf() + assert_matches_type(UnifiedResponse, smart, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_parse_cas_pdf_with_all_params(self, async_client: AsyncCasParser) -> None: + smart = await async_client.smart.parse_cas_pdf( + password="password", + pdf_file="pdf_file", + pdf_url="https://example.com", + ) + assert_matches_type(UnifiedResponse, smart, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_parse_cas_pdf(self, async_client: AsyncCasParser) -> None: + response = await async_client.smart.with_raw_response.parse_cas_pdf() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + smart = await response.parse() + assert_matches_type(UnifiedResponse, smart, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_parse_cas_pdf(self, async_client: AsyncCasParser) -> None: + async with async_client.smart.with_streaming_response.parse_cas_pdf() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + smart = await response.parse() + assert_matches_type(UnifiedResponse, smart, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_verify_token.py b/tests/api_resources/test_verify_token.py new file mode 100644 index 0000000..2ff8a90 --- /dev/null +++ b/tests/api_resources/test_verify_token.py @@ -0,0 +1,80 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import VerifyTokenVerifyResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestVerifyToken: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_verify(self, client: CasParser) -> None: + verify_token = client.verify_token.verify() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_verify(self, client: CasParser) -> None: + response = client.verify_token.with_raw_response.verify() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + verify_token = response.parse() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_verify(self, client: CasParser) -> None: + with client.verify_token.with_streaming_response.verify() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + verify_token = response.parse() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncVerifyToken: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_verify(self, async_client: AsyncCasParser) -> None: + verify_token = await async_client.verify_token.verify() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_verify(self, async_client: AsyncCasParser) -> None: + response = await async_client.verify_token.with_raw_response.verify() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + verify_token = await response.parse() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_verify(self, async_client: AsyncCasParser) -> None: + async with async_client.verify_token.with_streaming_response.verify() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + verify_token = await response.parse() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index 6f70fd4..ef1fd91 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -691,6 +691,18 @@ def test_base_url_env(self) -> None: client = CasParser(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" + # explicit environment arg requires explicitness + with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + CasParser(api_key=api_key, _strict_response_validation=True, environment="production") + + client = CasParser( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://portfolio-parser.api.casparser.in") + + client.close() + @pytest.mark.parametrize( "client", [ @@ -851,20 +863,20 @@ def test_parse_retry_after_header( @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: CasParser) -> None: - respx_mock.post("/v4/smart/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/credits").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.cas_parser.with_streaming_response.smart_parse().__enter__() + client.credits.with_streaming_response.check().__enter__() assert _get_open_connections(client) == 0 @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: CasParser) -> None: - respx_mock.post("/v4/smart/parse").mock(return_value=httpx.Response(500)) + respx_mock.post("/credits").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.cas_parser.with_streaming_response.smart_parse().__enter__() + client.credits.with_streaming_response.check().__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -891,9 +903,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) + respx_mock.post("/credits").mock(side_effect=retry_handler) - response = client.cas_parser.with_raw_response.smart_parse() + response = client.credits.with_raw_response.check() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -915,9 +927,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) + respx_mock.post("/credits").mock(side_effect=retry_handler) - response = client.cas_parser.with_raw_response.smart_parse(extra_headers={"x-stainless-retry-count": Omit()}) + response = client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -938,9 +950,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) + respx_mock.post("/credits").mock(side_effect=retry_handler) - response = client.cas_parser.with_raw_response.smart_parse(extra_headers={"x-stainless-retry-count": "42"}) + response = client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1580,6 +1592,18 @@ async def test_base_url_env(self) -> None: client = AsyncCasParser(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" + # explicit environment arg requires explicitness + with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + AsyncCasParser(api_key=api_key, _strict_response_validation=True, environment="production") + + client = AsyncCasParser( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://portfolio-parser.api.casparser.in") + + await client.close() + @pytest.mark.parametrize( "client", [ @@ -1751,10 +1775,10 @@ async def test_parse_retry_after_header( async def test_retrying_timeout_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncCasParser ) -> None: - respx_mock.post("/v4/smart/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/credits").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.cas_parser.with_streaming_response.smart_parse().__aenter__() + await async_client.credits.with_streaming_response.check().__aenter__() assert _get_open_connections(async_client) == 0 @@ -1763,10 +1787,10 @@ async def test_retrying_timeout_errors_doesnt_leak( async def test_retrying_status_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncCasParser ) -> None: - respx_mock.post("/v4/smart/parse").mock(return_value=httpx.Response(500)) + respx_mock.post("/credits").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.cas_parser.with_streaming_response.smart_parse().__aenter__() + await async_client.credits.with_streaming_response.check().__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1793,9 +1817,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) + respx_mock.post("/credits").mock(side_effect=retry_handler) - response = await client.cas_parser.with_raw_response.smart_parse() + response = await client.credits.with_raw_response.check() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1817,11 +1841,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) + respx_mock.post("/credits").mock(side_effect=retry_handler) - response = await client.cas_parser.with_raw_response.smart_parse( - extra_headers={"x-stainless-retry-count": Omit()} - ) + response = await client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1842,11 +1864,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/smart/parse").mock(side_effect=retry_handler) + respx_mock.post("/credits").mock(side_effect=retry_handler) - response = await client.cas_parser.with_raw_response.smart_parse( - extra_headers={"x-stainless-retry-count": "42"} - ) + response = await client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..5ca0453 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1829 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version < '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", +] +conflicts = [[ + { package = "cas-parser", group = "pydantic-v1" }, + { package = "cas-parser", group = "pydantic-v2" }, +]] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/446655656861d3e7e2c32bfcf160c7aa9e9dc63776a691b124dba65cdd77/aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e", size = 741433, upload-time = "2026-01-03T17:32:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/cb/49/773c4b310b5140d2fb5e79bb0bf40b7b41dad80a288ca1a8759f5f72bda9/aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7", size = 497332, upload-time = "2026-01-03T17:32:28.37Z" }, + { url = "https://files.pythonhosted.org/packages/bc/31/1dcbc4b83a4e6f76a0ad883f07f21ffbfe29750c89db97381701508c9f45/aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02", size = 492365, upload-time = "2026-01-03T17:32:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b5/b50657496c8754482cd7964e50aaf3aa84b3db61ed45daec4c1aec5b94b4/aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43", size = 1660440, upload-time = "2026-01-03T17:32:32.586Z" }, + { url = "https://files.pythonhosted.org/packages/2a/73/9b69e5139d89d75127569298931444ad78ea86a5befd5599780b1e9a6880/aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6", size = 1632740, upload-time = "2026-01-03T17:32:34.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/3ea9b5af694b4e3aec0d0613a806132ca744747146fca68e96bf056f61a7/aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce", size = 1719782, upload-time = "2026-01-03T17:32:37.737Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/46b3b06e60851cbb71efb0f79a3267279cbef7b12c58e68a1e897f269cca/aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80", size = 1813527, upload-time = "2026-01-03T17:32:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/36/23/71ceb78c769ed65fe4c697692de232b63dab399210678d2b00961ccb0619/aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a", size = 1661268, upload-time = "2026-01-03T17:32:42.082Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/86e929523d955e85ebab7c0e2b9e0cb63604cfc27dc3280e10d0063cf682/aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6", size = 1552742, upload-time = "2026-01-03T17:32:44.622Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/3f5987cba1bab6bd151f0d97aa60f0ce04d3c83316692a6bb6ba2fb69f92/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558", size = 1632918, upload-time = "2026-01-03T17:32:46.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/2c/7e1e85121f2e31ee938cb83a8f32dfafd4908530c10fabd6d46761c12ac7/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7", size = 1644446, upload-time = "2026-01-03T17:32:49.063Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/ce6133d423ad0e8ca976a7c848f7146bca3520eea4ccf6b95e2d077c9d20/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877", size = 1689487, upload-time = "2026-01-03T17:32:51.113Z" }, + { url = "https://files.pythonhosted.org/packages/50/f7/ff7a27c15603d460fd1366b3c22054f7ae4fa9310aca40b43bde35867fcd/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3", size = 1540715, upload-time = "2026-01-03T17:32:53.38Z" }, + { url = "https://files.pythonhosted.org/packages/17/02/053f11346e5b962e6d8a1c4f8c70c29d5970a1b4b8e7894c68e12c27a57f/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704", size = 1711835, upload-time = "2026-01-03T17:32:56.088Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/9b9761ddf276fd6708d13720197cbac19b8d67ecfa9116777924056cfcaa/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f", size = 1649593, upload-time = "2026-01-03T17:32:58.181Z" }, + { url = "https://files.pythonhosted.org/packages/ae/72/5d817e9ea218acae12a5e3b9ad1178cf0c12fc3570c0b47eea2daf95f9ea/aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1", size = 434831, upload-time = "2026-01-03T17:33:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/39/cb/22659d9bf3149b7a2927bc2769cc9c8f8f5a80eba098398e03c199a43a85/aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538", size = 457697, upload-time = "2026-01-03T17:33:03.167Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "cas-parser" +version = "1.2.1" +source = { editable = "." } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-10-cas-parser-pydantic-v1'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +aiohttp = [ + { name = "aiohttp" }, + { name = "httpx-aiohttp" }, +] + +[package.dev-dependencies] +dev = [ + { name = "dirty-equals" }, + { name = "importlib-metadata" }, + { name = "mypy" }, + { name = "pyright" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pytest-xdist" }, + { name = "respx" }, + { name = "rich" }, + { name = "ruff" }, + { name = "time-machine", version = "2.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "time-machine", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +pydantic-v1 = [ + { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" } }, +] +pydantic-v2 = [ + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" } }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", marker = "extra == 'aiohttp'" }, + { name = "anyio", specifier = ">=3.5.0,<5" }, + { name = "distro", specifier = ">=1.7.0,<2" }, + { name = "httpx", specifier = ">=0.23.0,<1" }, + { name = "httpx-aiohttp", marker = "extra == 'aiohttp'", specifier = ">=0.1.9" }, + { name = "pydantic", specifier = ">=1.9.0,<3" }, + { name = "sniffio" }, + { name = "typing-extensions", specifier = ">=4.10,<5" }, +] +provides-extras = ["aiohttp"] + +[package.metadata.requires-dev] +dev = [ + { name = "dirty-equals", specifier = ">=0.6.0" }, + { name = "importlib-metadata", specifier = ">=6.7.0" }, + { name = "mypy", specifier = "==1.17" }, + { name = "pyright", specifier = "==1.1.399" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-xdist", specifier = ">=3.6.1" }, + { name = "respx" }, + { name = "rich", specifier = ">=13.7.1" }, + { name = "ruff" }, + { name = "time-machine" }, +] +pydantic-v1 = [{ name = "pydantic", specifier = ">=1.9.0,<2" }] +pydantic-v2 = [ + { name = "pydantic", marker = "python_full_version < '3.14'", specifier = "~=2.0" }, + { name = "pydantic", marker = "python_full_version >= '3.14'", specifier = "~=2.12" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dirty-equals" +version = "0.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/1d/c5913ac9d6615515a00f4bdc71356d302437cb74ff2e9aaccd3c14493b78/dirty_equals-0.11.tar.gz", hash = "sha256:f4ac74ee88f2d11e2fa0f65eb30ee4f07105c5f86f4dc92b09eb1138775027c3", size = 128067, upload-time = "2025-11-17T01:51:24.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/8d/dbff05239043271dbeace563a7686212a3dd517864a35623fe4d4a64ca19/dirty_equals-0.11-py3-none-any.whl", hash = "sha256:b1d7093273fc2f9be12f443a8ead954ef6daaf6746fd42ef3a5616433ee85286", size = 28051, upload-time = "2025-11-17T01:51:22.849Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/ae5cdac87a00962122ea37bb346d41b66aec05f9ce328fa2b9e216f8967b/frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", size = 86967, upload-time = "2025-10-06T05:37:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/8a/10/17059b2db5a032fd9323c41c39e9d1f5f9d0c8f04d1e4e3e788573086e61/frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", size = 49984, upload-time = "2025-10-06T05:37:57.049Z" }, + { url = "https://files.pythonhosted.org/packages/4b/de/ad9d82ca8e5fa8f0c636e64606553c79e2b859ad253030b62a21fe9986f5/frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", size = 50240, upload-time = "2025-10-06T05:37:58.145Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/3dfb7767c2a67d123650122b62ce13c731b6c745bc14424eea67678b508c/frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", size = 219472, upload-time = "2025-10-06T05:37:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bf/5bf23d913a741b960d5c1dac7c1985d8a2a1d015772b2d18ea168b08e7ff/frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", size = 221531, upload-time = "2025-10-06T05:38:00.521Z" }, + { url = "https://files.pythonhosted.org/packages/d0/03/27ec393f3b55860859f4b74cdc8c2a4af3dbf3533305e8eacf48a4fd9a54/frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", size = 219211, upload-time = "2025-10-06T05:38:01.842Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ad/0fd00c404fa73fe9b169429e9a972d5ed807973c40ab6b3cf9365a33d360/frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", size = 231775, upload-time = "2025-10-06T05:38:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c3/86962566154cb4d2995358bc8331bfc4ea19d07db1a96f64935a1607f2b6/frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", size = 236631, upload-time = "2025-10-06T05:38:04.609Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/6ffad161dbd83782d2c66dc4d378a9103b31770cb1e67febf43aea42d202/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", size = 218632, upload-time = "2025-10-06T05:38:05.917Z" }, + { url = "https://files.pythonhosted.org/packages/58/b2/4677eee46e0a97f9b30735e6ad0bf6aba3e497986066eb68807ac85cf60f/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", size = 235967, upload-time = "2025-10-06T05:38:07.614Z" }, + { url = "https://files.pythonhosted.org/packages/05/f3/86e75f8639c5a93745ca7addbbc9de6af56aebb930d233512b17e46f6493/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", size = 228799, upload-time = "2025-10-06T05:38:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/30/00/39aad3a7f0d98f5eb1d99a3c311215674ed87061aecee7851974b335c050/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", size = 230566, upload-time = "2025-10-06T05:38:10.52Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4d/aa144cac44568d137846ddc4d5210fb5d9719eb1d7ec6fa2728a54b5b94a/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", size = 217715, upload-time = "2025-10-06T05:38:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/64/4c/8f665921667509d25a0dd72540513bc86b356c95541686f6442a3283019f/frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", size = 39933, upload-time = "2025-10-06T05:38:13.061Z" }, + { url = "https://files.pythonhosted.org/packages/79/bd/bcc926f87027fad5e59926ff12d136e1082a115025d33c032d1cd69ab377/frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", size = 44121, upload-time = "2025-10-06T05:38:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/4c/07/9c2e4eb7584af4b705237b971b89a4155a8e57599c4483a131a39256a9a0/frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", size = 40312, upload-time = "2025-10-06T05:38:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-aiohttp" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/2c/b894861cecf030fb45675ea24aa55b5722e97c602a163d872fca66c5a6d8/httpx_aiohttp-0.1.12.tar.gz", hash = "sha256:81feec51fd82c0ecfa0e9aaf1b1a6c2591260d5e2bcbeb7eb0277a78e610df2c", size = 275945, upload-time = "2025-12-12T10:12:15.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8d/85c9701e9af72ca132a1783e2a54364a90c6da832304416a30fc11196ab2/httpx_aiohttp-0.1.12-py3-none-any.whl", hash = "sha256:5b0eac39a7f360fa7867a60bcb46bb1024eada9c01cbfecdb54dc1edb3fb7141", size = 6367, upload-time = "2025-12-12T10:12:14.018Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153, upload-time = "2025-10-06T14:48:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993, upload-time = "2025-10-06T14:48:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607, upload-time = "2025-10-06T14:48:29.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847, upload-time = "2025-10-06T14:48:32.107Z" }, + { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616, upload-time = "2025-10-06T14:48:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333, upload-time = "2025-10-06T14:48:35.9Z" }, + { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239, upload-time = "2025-10-06T14:48:37.302Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618, upload-time = "2025-10-06T14:48:38.963Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655, upload-time = "2025-10-06T14:48:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245, upload-time = "2025-10-06T14:48:41.848Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523, upload-time = "2025-10-06T14:48:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129, upload-time = "2025-10-06T14:48:45.225Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999, upload-time = "2025-10-06T14:48:46.703Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711, upload-time = "2025-10-06T14:48:48.146Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504, upload-time = "2025-10-06T14:48:49.447Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422, upload-time = "2025-10-06T14:48:50.789Z" }, + { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050, upload-time = "2025-10-06T14:48:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153, upload-time = "2025-10-06T14:48:53.146Z" }, + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/90/d7/4cf84257902265c4250769ac49f4eaab81c182ee9aff8bf59d2714dbb174/multidict-6.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c", size = 77073, upload-time = "2025-10-06T14:51:57.386Z" }, + { url = "https://files.pythonhosted.org/packages/6d/51/194e999630a656e76c2965a1590d12faa5cd528170f2abaa04423e09fe8d/multidict-6.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40", size = 44928, upload-time = "2025-10-06T14:51:58.791Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6b/2a195373c33068c9158e0941d0b46cfcc9c1d894ca2eb137d1128081dff0/multidict-6.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851", size = 44581, upload-time = "2025-10-06T14:52:00.174Z" }, + { url = "https://files.pythonhosted.org/packages/69/7b/7f4f2e644b6978bf011a5fd9a5ebb7c21de3f38523b1f7897d36a1ac1311/multidict-6.7.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687", size = 239901, upload-time = "2025-10-06T14:52:02.416Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b5/952c72786710a031aa204a9adf7db66d7f97a2c6573889d58b9e60fe6702/multidict-6.7.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5", size = 240534, upload-time = "2025-10-06T14:52:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ef/109fe1f2471e4c458c74242c7e4a833f2d9fc8a6813cd7ee345b0bad18f9/multidict-6.7.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb", size = 219545, upload-time = "2025-10-06T14:52:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/42/bd/327d91288114967f9fe90dc53de70aa3fec1b9073e46aa32c4828f771a87/multidict-6.7.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6", size = 251187, upload-time = "2025-10-06T14:52:08.049Z" }, + { url = "https://files.pythonhosted.org/packages/f4/13/a8b078ebbaceb7819fd28cd004413c33b98f1b70d542a62e6a00b74fb09f/multidict-6.7.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e", size = 249379, upload-time = "2025-10-06T14:52:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6d/ab12e1246be4d65d1f55de1e6f6aaa9b8120eddcfdd1d290439c7833d5ce/multidict-6.7.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e", size = 239241, upload-time = "2025-10-06T14:52:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/079a93625208c173b8fa756396814397c0fd9fee61ef87b75a748820b86e/multidict-6.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32", size = 237418, upload-time = "2025-10-06T14:52:13.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/03777c2212274aa9440918d604dc9d6af0e6b4558c611c32c3dcf1a13870/multidict-6.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c", size = 232987, upload-time = "2025-10-06T14:52:15.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/00/11188b68d85a84e8050ee34724d6ded19ad03975caebe0c8dcb2829b37bf/multidict-6.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84", size = 240985, upload-time = "2025-10-06T14:52:17.317Z" }, + { url = "https://files.pythonhosted.org/packages/df/0c/12eef6aeda21859c6cdf7d75bd5516d83be3efe3d8cc45fd1a3037f5b9dc/multidict-6.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329", size = 246855, upload-time = "2025-10-06T14:52:19.096Z" }, + { url = "https://files.pythonhosted.org/packages/69/f6/076120fd8bb3975f09228e288e08bff6b9f1bfd5166397c7ba284f622ab2/multidict-6.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e", size = 241804, upload-time = "2025-10-06T14:52:21.166Z" }, + { url = "https://files.pythonhosted.org/packages/5f/51/41bb950c81437b88a93e6ddfca1d8763569ae861e638442838c4375f7497/multidict-6.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4", size = 235321, upload-time = "2025-10-06T14:52:23.208Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cf/5bbd31f055199d56c1f6b04bbadad3ccb24e6d5d4db75db774fc6d6674b8/multidict-6.7.0-cp39-cp39-win32.whl", hash = "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91", size = 41435, upload-time = "2025-10-06T14:52:24.735Z" }, + { url = "https://files.pythonhosted.org/packages/af/01/547ffe9c2faec91c26965c152f3fea6cff068b6037401f61d310cc861ff4/multidict-6.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f", size = 46193, upload-time = "2025-10-06T14:52:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/27/77/cfa5461d1d2651d6fc24216c92b4a21d4e385a41c46e0d9f3b070675167b/multidict-6.7.0-cp39-cp39-win_arm64.whl", hash = "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546", size = 43118, upload-time = "2025-10-06T14:52:27.876Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "mypy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/31/e762baa3b73905c856d45ab77b4af850e8159dffffd86a52879539a08c6b/mypy-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8e08de6138043108b3b18f09d3f817a4783912e48828ab397ecf183135d84d6", size = 10998313, upload-time = "2025-07-14T20:33:24.519Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c1/25b2f0d46fb7e0b5e2bee61ec3a47fe13eff9e3c2f2234f144858bbe6485/mypy-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce4a17920ec144647d448fc43725b5873548b1aae6c603225626747ededf582d", size = 10128922, upload-time = "2025-07-14T20:34:06.414Z" }, + { url = "https://files.pythonhosted.org/packages/02/78/6d646603a57aa8a2886df1b8881fe777ea60f28098790c1089230cd9c61d/mypy-1.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff25d151cc057fdddb1cb1881ef36e9c41fa2a5e78d8dd71bee6e4dcd2bc05b", size = 11913524, upload-time = "2025-07-14T20:33:19.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/19/dae6c55e87ee426fb76980f7e78484450cad1c01c55a1dc4e91c930bea01/mypy-1.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93468cf29aa9a132bceb103bd8475f78cacde2b1b9a94fd978d50d4bdf616c9a", size = 12650527, upload-time = "2025-07-14T20:32:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/86/e1/f916845a235235a6c1e4d4d065a3930113767001d491b8b2e1b61ca56647/mypy-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98189382b310f16343151f65dd7e6867386d3e35f7878c45cfa11383d175d91f", size = 12897284, upload-time = "2025-07-14T20:33:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/ae/dc/414760708a4ea1b096bd214d26a24e30ac5e917ef293bc33cdb6fe22d2da/mypy-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:c004135a300ab06a045c1c0d8e3f10215e71d7b4f5bb9a42ab80236364429937", size = 9506493, upload-time = "2025-07-14T20:34:01.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" }, + { url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/5f8ab461369b9e62157072156935cec9d272196556bdc7c2ff5f4c7c0f9b/mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06", size = 11070019, upload-time = "2025-07-14T20:32:07.99Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/c49c9e5a2ac0badcc54beb24e774d2499748302c9568f7f09e8730e953fa/mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a", size = 10114457, upload-time = "2025-07-14T20:33:47.285Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/fb3f9c939ad9beed3e328008b3fb90b20fda2cddc0f7e4c20dbefefc3b33/mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889", size = 11857838, upload-time = "2025-07-14T20:33:14.462Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/85607ab5137d65e4f54d9797b77d5a038ef34f714929cf8ad30b03f628df/mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba", size = 12731358, upload-time = "2025-07-14T20:32:25.579Z" }, + { url = "https://files.pythonhosted.org/packages/73/d0/341dbbfb35ce53d01f8f2969facbb66486cee9804048bf6c01b048127501/mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658", size = 12917480, upload-time = "2025-07-14T20:34:21.868Z" }, + { url = "https://files.pythonhosted.org/packages/64/63/70c8b7dbfc520089ac48d01367a97e8acd734f65bd07813081f508a8c94c/mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c", size = 9589666, upload-time = "2025-07-14T20:34:16.841Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a0/6263dd11941231f688f0a8f2faf90ceac1dc243d148d314a089d2fe25108/mypy-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63e751f1b5ab51d6f3d219fe3a2fe4523eaa387d854ad06906c63883fde5b1ab", size = 10988185, upload-time = "2025-07-14T20:33:04.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/13/b8f16d6b0dc80277129559c8e7dbc9011241a0da8f60d031edb0e6e9ac8f/mypy-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fb09d05e0f1c329a36dcd30e27564a3555717cde87301fae4fb542402ddfad", size = 10120169, upload-time = "2025-07-14T20:32:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/14/ef/978ba79df0d65af680e20d43121363cf643eb79b04bf3880d01fc8afeb6f/mypy-1.17.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72c34ce05ac3a1361ae2ebb50757fb6e3624032d91488d93544e9f82db0ed6c", size = 11918121, upload-time = "2025-07-14T20:33:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/55ef70b104151a0d8280474f05268ff0a2a79be8d788d5e647257d121309/mypy-1.17.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434ad499ad8dde8b2f6391ddfa982f41cb07ccda8e3c67781b1bfd4e5f9450a8", size = 12648821, upload-time = "2025-07-14T20:32:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/26/8c/7781fcd2e1eef48fbedd3a422c21fe300a8e03ed5be2eb4bd10246a77f4e/mypy-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f105f61a5eff52e137fd73bee32958b2add9d9f0a856f17314018646af838e97", size = 12896955, upload-time = "2025-07-14T20:32:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/03ac759dabe86e98ca7b6681f114f90ee03f3ff8365a57049d311bd4a4e3/mypy-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:ba06254a5a22729853209550d80f94e28690d5530c661f9416a68ac097b13fc4", size = 9512957, upload-time = "2025-07-14T20:33:28.619Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/0ebaec9003f5d619a7475165961f8e3083cf8644d704b60395df3601632d/propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", size = 80277, upload-time = "2025-10-08T19:48:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/04af97ac586b4ef6b9026c3fd36ee7798b737a832f5d3440a4280dcebd3a/propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", size = 45865, upload-time = "2025-10-08T19:48:37.859Z" }, + { url = "https://files.pythonhosted.org/packages/7c/19/b65d98ae21384518b291d9939e24a8aeac4fdb5101b732576f8f7540e834/propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", size = 47636, upload-time = "2025-10-08T19:48:39.038Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/317048c6d91c356c7154dca5af019e6effeb7ee15fa6a6db327cc19e12b4/propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", size = 201126, upload-time = "2025-10-08T19:48:40.774Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/0b2a7a5a6ee83292b4b997dbd80549d8ce7d40b6397c1646c0d9495f5a85/propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", size = 209837, upload-time = "2025-10-08T19:48:42.167Z" }, + { url = "https://files.pythonhosted.org/packages/a5/92/c699ac495a6698df6e497fc2de27af4b6ace10d8e76528357ce153722e45/propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", size = 215578, upload-time = "2025-10-08T19:48:43.56Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ee/14de81c5eb02c0ee4f500b4e39c4e1bd0677c06e72379e6ab18923c773fc/propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", size = 197187, upload-time = "2025-10-08T19:48:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/48dce9aaa6d8dd5a0859bad75158ec522546d4ac23f8e2f05fac469477dd/propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", size = 193478, upload-time = "2025-10-08T19:48:47.743Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/0516b563e801e1ace212afde869a0596a0d7115eec0b12d296d75633fb29/propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", size = 190650, upload-time = "2025-10-08T19:48:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/24/89/e0f7d4a5978cd56f8cd67735f74052f257dc471ec901694e430f0d1572fe/propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", size = 200251, upload-time = "2025-10-08T19:48:51.4Z" }, + { url = "https://files.pythonhosted.org/packages/06/7d/a1fac863d473876ed4406c914f2e14aa82d2f10dd207c9e16fc383cc5a24/propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781", size = 200919, upload-time = "2025-10-08T19:48:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4e/f86a256ff24944cf5743e4e6c6994e3526f6acfcfb55e21694c2424f758c/propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", size = 193211, upload-time = "2025-10-08T19:48:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/6e/3f/3fbad5f4356b068f1b047d300a6ff2c66614d7030f078cd50be3fec04228/propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", size = 38314, upload-time = "2025-10-08T19:48:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/a4/45/d78d136c3a3d215677abb886785aae744da2c3005bcb99e58640c56529b1/propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", size = 41912, upload-time = "2025-10-08T19:48:57.995Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/b0632941f25139f4e58450b307242951f7c2717a5704977c6d5323a800af/propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", size = 38450, upload-time = "2025-10-08T19:48:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pydantic" +version = "1.10.26" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-10-cas-parser-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/da/fd89f987a376c807cd81ea0eff4589aade783bbb702637b4734ef2c743a2/pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e", size = 357906, upload-time = "2025-12-18T15:47:46.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/08/2587a6d4314e7539eec84acd062cb7b037638edb57a0335d20e4c5b8878c/pydantic-1.10.26-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7ae36fa0ecef8d39884120f212e16c06bb096a38f523421278e2f39c1784546", size = 2444588, upload-time = "2025-12-18T15:46:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/47/e6/10df5f08c105bcbb4adbee7d1108ff4b347702b110fed058f6a03f1c6b73/pydantic-1.10.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d95a76cf503f0f72ed7812a91de948440b2bf564269975738a4751e4fadeb572", size = 2255972, upload-time = "2025-12-18T15:46:31.72Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/fdb961e7adc2c31f394feba6f560ef2c74c446f0285e2c2eb87d2b7206c7/pydantic-1.10.26-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a943ce8e00ad708ed06a1d9df5b4fd28f5635a003b82a4908ece6f24c0b18464", size = 2857175, upload-time = "2025-12-18T15:46:34Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6c/f21e27dda475d4c562bd01b5874284dd3180f336c1e669413b743ca8b278/pydantic-1.10.26-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:465ad8edb29b15c10b779b16431fe8e77c380098badf6db367b7a1d3e572cf53", size = 2947001, upload-time = "2025-12-18T15:46:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f6/27ea206232cbb6ec24dc4e4e8888a9a734f96a1eaf13504be4b30ef26aa7/pydantic-1.10.26-cp310-cp310-win_amd64.whl", hash = "sha256:80e6be6272839c8a7641d26ad569ab77772809dd78f91d0068dc0fc97f071945", size = 2066217, upload-time = "2025-12-18T15:46:37.614Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c1/d521e64c8130e1ad9d22c270bed3fabcc0940c9539b076b639c88fd32a8d/pydantic-1.10.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:116233e53889bcc536f617e38c1b8337d7fa9c280f0fd7a4045947515a785637", size = 2428347, upload-time = "2025-12-18T15:46:39.41Z" }, + { url = "https://files.pythonhosted.org/packages/2c/08/f4b804a00c16e3ea994cb640a7c25c579b4f1fa674cde6a19fa0dfb0ae4f/pydantic-1.10.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3cfdd361addb6eb64ccd26ac356ad6514cee06a61ab26b27e16b5ed53108f77", size = 2212605, upload-time = "2025-12-18T15:46:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/0df4b9efef29bbc5e39f247fcba99060d15946b4463d82a5589cf7923d71/pydantic-1.10.26-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e4451951a9a93bf9a90576f3e25240b47ee49ab5236adccb8eff6ac943adf0f", size = 2753560, upload-time = "2025-12-18T15:46:43.215Z" }, + { url = "https://files.pythonhosted.org/packages/68/66/6ab6c1d3a116d05d2508fce64f96e35242938fac07544d611e11d0d363a0/pydantic-1.10.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9858ed44c6bea5f29ffe95308db9e62060791c877766c67dd5f55d072c8612b5", size = 2859235, upload-time = "2025-12-18T15:46:45.112Z" }, + { url = "https://files.pythonhosted.org/packages/61/4e/f1676bb0fcdf6ed2ce4670d7d1fc1d6c3a06d84497644acfbe02649503f1/pydantic-1.10.26-cp311-cp311-win_amd64.whl", hash = "sha256:ac1089f723e2106ebde434377d31239e00870a7563245072968e5af5cc4d33df", size = 2066646, upload-time = "2025-12-18T15:46:46.816Z" }, + { url = "https://files.pythonhosted.org/packages/02/6c/cd97a5a776c4515e6ee2ae81c2f2c5be51376dda6c31f965d7746ce0019f/pydantic-1.10.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:468d5b9cacfcaadc76ed0a4645354ab6f263ec01a63fb6d05630ea1df6ae453f", size = 2433795, upload-time = "2025-12-18T15:46:49.321Z" }, + { url = "https://files.pythonhosted.org/packages/47/12/de20affa30dcef728fcf9cc98e13ff4438c7a630de8d2f90eb38eba0891c/pydantic-1.10.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2c1b0b914be31671000ca25cf7ea17fcaaa68cfeadf6924529c5c5aa24b7ab1f", size = 2227387, upload-time = "2025-12-18T15:46:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/9d65dcc5b8c17ba590f1f9f486e9306346831902318b7ee93f63516f4003/pydantic-1.10.26-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15b13b9f8ba8867095769e1156e0d7fbafa1f65b898dd40fd1c02e34430973cb", size = 2629594, upload-time = "2025-12-18T15:46:53.42Z" }, + { url = "https://files.pythonhosted.org/packages/3f/76/acb41409356789e23e1a7ef58f93821410c96409183ce314ddb58d97f23e/pydantic-1.10.26-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad7025ca324ae263d4313998e25078dcaec5f9ed0392c06dedb57e053cc8086b", size = 2745305, upload-time = "2025-12-18T15:46:55.987Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/a98c0c5e527a66057d969fedd61675223c7975ade61acebbca9f1abd6dc0/pydantic-1.10.26-cp312-cp312-win_amd64.whl", hash = "sha256:4482b299874dabb88a6c3759e3d85c6557c407c3b586891f7d808d8a38b66b9c", size = 1937647, upload-time = "2025-12-18T15:46:57.905Z" }, + { url = "https://files.pythonhosted.org/packages/28/b9/17a5a5a421c23ac27486b977724a42c9d5f8b7f0f4aab054251066223900/pydantic-1.10.26-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ae7913bb40a96c87e3d3f6fe4e918ef53bf181583de4e71824360a9b11aef1c", size = 2494599, upload-time = "2025-12-18T15:47:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8e/6e3bd4241076cf227b443d7577245dd5d181ecf40b3182fcb908bc8c197d/pydantic-1.10.26-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8154c13f58d4de5d3a856bb6c909c7370f41fb876a5952a503af6b975265f4ba", size = 2254391, upload-time = "2025-12-18T15:47:02.268Z" }, + { url = "https://files.pythonhosted.org/packages/a8/30/a1c4092eda2145ecbead6c92db489b223e101e1ba0da82576d0cf73dd422/pydantic-1.10.26-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8af0507bf6118b054a9765fb2e402f18a8b70c964f420d95b525eb711122d62", size = 2609445, upload-time = "2025-12-18T15:47:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2a/0491f1729ee4b7b6bc859ec22f69752f0c09bee1b66ac6f5f701136f34c3/pydantic-1.10.26-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dcb5a7318fb43189fde6af6f21ac7149c4bcbcfffc54bc87b5becddc46084847", size = 2732124, upload-time = "2025-12-18T15:47:07.464Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/b59f3b2f84e1df2b04ae768a1bb04d9f0288ff71b67cdcbb07683757b2c0/pydantic-1.10.26-cp313-cp313-win_amd64.whl", hash = "sha256:71cde228bc0600cf8619f0ee62db050d1880dcc477eba0e90b23011b4ee0f314", size = 1939888, upload-time = "2025-12-18T15:47:09.618Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8b/0c3dc02d4b97790b0f199bf933f677c14e7be4a8d21307c5f2daae06aa41/pydantic-1.10.26-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6b40730cc81d53d515dc0b8bb5c9b43fadb9bed46de4a3c03bd95e8571616dba", size = 2502689, upload-time = "2025-12-18T15:47:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9d/d31aeea45542b2ae4b09ecba92b88aaba696b801c31919811aa979a1242d/pydantic-1.10.26-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c3bbb9c0eecdf599e4db9b372fa9cc55be12e80a0d9c6d307950a39050cb0e37", size = 2269494, upload-time = "2025-12-18T15:47:14.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/c1/3a4d069593283ca4dd0006039ba33644e21e432cddc09da706ac50441610/pydantic-1.10.26-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc2e3fe7bc4993626ef6b6fa855defafa1d6f8996aa1caef2deb83c5ac4d043a", size = 2620047, upload-time = "2025-12-18T15:47:17.089Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0e/340c3d29197d99c15ab04093d43bb9c9d0fd17c2a34b80cb9d36ed732b09/pydantic-1.10.26-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:36d9e46b588aaeb1dcd2409fa4c467fe0b331f3cc9f227b03a7a00643704e962", size = 2747625, upload-time = "2025-12-18T15:47:19.21Z" }, + { url = "https://files.pythonhosted.org/packages/1e/58/f12ab3727339b172c830b32151919456b67787cdfe8808b2568b322fb15c/pydantic-1.10.26-cp314-cp314-win_amd64.whl", hash = "sha256:81ce3c8616d12a7be31b4aadfd3434f78f6b44b75adbfaec2fe1ad4f7f999b8c", size = 1976436, upload-time = "2025-12-18T15:47:21.384Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8a/3a5a6267d5f03617b5c0f1985aa9fdfbafd33a50ef6dadd866a15ed4d123/pydantic-1.10.26-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:502b9d30d18a2dfaf81b7302f6ba0e5853474b1c96212449eb4db912cb604b7d", size = 2457039, upload-time = "2025-12-18T15:47:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/343ac0db26918a033ac6256c036d72c3b6eb1196b7de622e2e8a94b19079/pydantic-1.10.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d8f6087bf697dec3bf7ffcd7fe8362674f16519f3151789f33cbe8f1d19fc15", size = 2266441, upload-time = "2025-12-18T15:47:36.807Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/1ab48136578608dba2f2a62e452f3db2083b474d4e49be5749c6ae0c123c/pydantic-1.10.26-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd40a99c358419910c85e6f5d22f9c56684c25b5e7abc40879b3b4a52f34ae90", size = 2869383, upload-time = "2025-12-18T15:47:38.883Z" }, + { url = "https://files.pythonhosted.org/packages/a2/25/41dbf1bffc31eb242cece8080561a4133eaeb513372dec36a84477a3fb71/pydantic-1.10.26-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ce3293b86ca9f4125df02ff0a70be91bc7946522467cbd98e7f1493f340616ba", size = 2963582, upload-time = "2025-12-18T15:47:40.854Z" }, + { url = "https://files.pythonhosted.org/packages/61/2f/f072ae160a300c85eb9f059915101fd33dacf12d8df08c2b804acb3b95d1/pydantic-1.10.26-cp39-cp39-win_amd64.whl", hash = "sha256:1a4e3062b71ab1d5df339ba12c48f9ed5817c5de6cb92a961dd5c64bb32e7b96", size = 2075530, upload-time = "2025-12-18T15:47:43.181Z" }, + { url = "https://files.pythonhosted.org/packages/1f/98/556e82f00b98486def0b8af85da95e69d2be7e367cf2431408e108bc3095/pydantic-1.10.26-py3-none-any.whl", hash = "sha256:c43ad70dc3ce7787543d563792426a16fd7895e14be4b194b5665e36459dd917", size = 166975, upload-time = "2025-12-18T15:47:44.927Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", +] +dependencies = [ + { name = "annotated-types", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, + { name = "pydantic-core", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, + { name = "typing-inspection", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, + { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.399" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954, upload-time = "2025-04-10T04:40:25.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584, upload-time = "2025-04-10T04:40:23.502Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.10' and python_full_version < '3.13') or (python_full_version < '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2') or (python_full_version >= '3.13' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "time-machine" +version = "2.19.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "python-dateutil", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/1b5fdd165f61b67f445fac2a7feb0c655118edef429cd09ff5a8067f7f1d/time_machine-2.19.0.tar.gz", hash = "sha256:7c5065a8b3f2bbb449422c66ef71d114d3f909c276a6469642ecfffb6a0fcd29", size = 14576, upload-time = "2025-08-19T17:22:08.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8f/19125611ebbcb3a14da14cd982b9eb4573e2733db60c9f1fbf6a39534f40/time_machine-2.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b5169018ef47206997b46086ce01881cd3a4666fd2998c9d76a87858ca3e49e9", size = 19659, upload-time = "2025-08-19T17:20:30.062Z" }, + { url = "https://files.pythonhosted.org/packages/74/da/9b0a928321e7822a3ff96dbd1eae089883848e30e9e1b149b85fb96ba56b/time_machine-2.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85bb7ed440fccf6f6d0c8f7d68d849e7c3d1f771d5e0b2cdf871fa6561da569f", size = 15157, upload-time = "2025-08-19T17:20:31.931Z" }, + { url = "https://files.pythonhosted.org/packages/36/ff/d7e943422038f5f2161fe2c2d791e64a45be691ef946020b20f3a6efc4d4/time_machine-2.19.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a3b12028af1cdc09ccd595be2168b7b26f206c1e190090b048598fbe278beb8e", size = 32860, upload-time = "2025-08-19T17:20:33.241Z" }, + { url = "https://files.pythonhosted.org/packages/fc/80/2b0f1070ed9808ee7da7a6da62a4a0b776957cb4d861578348f86446e778/time_machine-2.19.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c261f073086cf081d1443cbf7684148c662659d3d139d06b772bfe3fe7cc71a6", size = 34510, upload-time = "2025-08-19T17:20:34.221Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b4/48038691c8d89924b36c83335a73adeeb68c884f5a1da08a5b17b8a956f3/time_machine-2.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:011954d951230a9f1079f22b39ed1a3a9abb50ee297dfb8c557c46351659d94d", size = 36204, upload-time = "2025-08-19T17:20:35.163Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/60e8adb541df195e83cb74b720b2cfb1f22ed99c5a7f8abf2a9ab3442cb5/time_machine-2.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b0f83308b29c7872006803f2e77318874eb84d0654f2afe0e48e3822e7a2e39b", size = 34936, upload-time = "2025-08-19T17:20:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/72/e8cee59c6cd99dd3b25b8001a0253e779a286aa8f44d5b40777cbd66210b/time_machine-2.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:39733ef844e2984620ec9382a42d00cccc4757d75a5dd572be8c2572e86e50b9", size = 32932, upload-time = "2025-08-19T17:20:37.901Z" }, + { url = "https://files.pythonhosted.org/packages/2c/eb/83f300d93c1504965d944e03679f1c943a923bce2d0fdfadef0e2e22cc13/time_machine-2.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8db99f6334432e9ffbf00c215caf2ae9773f17cec08304d77e9e90febc3507b", size = 34010, upload-time = "2025-08-19T17:20:39.202Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/f35f2500e04daac5033a22fbfd17e68467822b8406ee77966bf222ccaa26/time_machine-2.19.0-cp310-cp310-win32.whl", hash = "sha256:72bf66cd19e27ffd26516b9cbe676d50c2e0b026153289765dfe0cf406708128", size = 17121, upload-time = "2025-08-19T17:20:40.108Z" }, + { url = "https://files.pythonhosted.org/packages/db/df/32d3e0404be1760a64a44caab2af34b07e952bfe00a23134fea9ddba3e8a/time_machine-2.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:46f1c945934ce3d6b4f388b8e581fce7f87ec891ea90d7128e19520e434f96f0", size = 17957, upload-time = "2025-08-19T17:20:41.079Z" }, + { url = "https://files.pythonhosted.org/packages/66/df/598a71a1afb4b509a4587273b76590b16d9110a3e9106f01eedc68d02bb2/time_machine-2.19.0-cp310-cp310-win_arm64.whl", hash = "sha256:fb4897c7a5120a4fd03f0670f332d83b7e55645886cd8864a71944c4c2e5b35b", size = 16821, upload-time = "2025-08-19T17:20:41.967Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ed/4815ebcc9b6c14273f692b9be38a9b09eae52a7e532407cc61a51912b121/time_machine-2.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ee91664880434d98e41585c3446dac7180ec408c786347451ddfca110d19296", size = 19342, upload-time = "2025-08-19T17:20:43.207Z" }, + { url = "https://files.pythonhosted.org/packages/ee/08/154cce8b11b60d8238b0b751b8901d369999f4e8f7c3a5f917caa5d95b0b/time_machine-2.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed3732b83a893d1c7b8cabde762968b4dc5680ee0d305b3ecca9bb516f4e3862", size = 14978, upload-time = "2025-08-19T17:20:44.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/b689d8c8eeca7af375cfcd64973e49e83aa817cc00f80f98548d42c0eb50/time_machine-2.19.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6ba0303e9cc9f7f947e344f501e26bedfb68fab521e3c2729d370f4f332d2d55", size = 30964, upload-time = "2025-08-19T17:20:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/80/91/38bf9c79674e95ce32e23c267055f281dff651eec77ed32a677db3dc011a/time_machine-2.19.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2851825b524a988ee459c37c1c26bdfaa7eff78194efb2b562ea497a6f375b0a", size = 32606, upload-time = "2025-08-19T17:20:46.693Z" }, + { url = "https://files.pythonhosted.org/packages/19/4a/e9222d85d4de68975a5e799f539a9d32f3a134a9101fca0a61fa6aa33d68/time_machine-2.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68d32b09ecfd7fef59255c091e8e7c24dd117f882c4880b5c7ab8c5c32a98f89", size = 34405, upload-time = "2025-08-19T17:20:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/09480d608d42d6876f9ff74593cfc9197a7eb2c31381a74fb2b145575b65/time_machine-2.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60c46ab527bf2fa144b530f639cc9e12803524c9e1f111dc8c8f493bb6586eeb", size = 33181, upload-time = "2025-08-19T17:20:48.937Z" }, + { url = "https://files.pythonhosted.org/packages/84/64/f9359e000fad32d9066305c48abc527241d608bcdf77c19d67d66e268455/time_machine-2.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:56f26ab9f0201c453d18fe76bb7d1cf05fe58c1b9d9cb0c7d243d05132e01292", size = 31036, upload-time = "2025-08-19T17:20:50.276Z" }, + { url = "https://files.pythonhosted.org/packages/71/0d/fab2aacec71e3e482bd7fce0589381f9414a4a97f8766bddad04ad047b7b/time_machine-2.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6c806cf3c1185baa1d807b7f51bed0db7a6506832c961d5d1b4c94c775749bc0", size = 32145, upload-time = "2025-08-19T17:20:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/44/fb/faeba2405fb27553f7b28db441a500e2064ffdb2dcba001ee315fdd2c121/time_machine-2.19.0-cp311-cp311-win32.whl", hash = "sha256:b30039dfd89855c12138095bee39c540b4633cbc3684580d684ef67a99a91587", size = 17004, upload-time = "2025-08-19T17:20:52.38Z" }, + { url = "https://files.pythonhosted.org/packages/2f/84/87e483d660ca669426192969280366635c845c3154a9fe750be546ed3afc/time_machine-2.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:13ed8b34430f1de79905877f5600adffa626793ab4546a70a99fb72c6a3350d8", size = 17822, upload-time = "2025-08-19T17:20:53.348Z" }, + { url = "https://files.pythonhosted.org/packages/41/f4/ebf7bbf5047854a528adaf54a5e8780bc5f7f0104c298ab44566a3053bf8/time_machine-2.19.0-cp311-cp311-win_arm64.whl", hash = "sha256:cc29a50a0257d8750b08056b66d7225daab47606832dea1a69e8b017323bf511", size = 16680, upload-time = "2025-08-19T17:20:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/9b/aa/7e00614d339e4d687f6e96e312a1566022528427d237ec639df66c4547bc/time_machine-2.19.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c85cf437dc3c07429456d8d6670ac90ecbd8241dcd0fbf03e8db2800576f91ff", size = 19308, upload-time = "2025-08-19T17:20:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/ab/3c/bde3c757394f5bca2fbc1528d4117960a26c38f9b160bf471b38d2378d8f/time_machine-2.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d9238897e8ef54acdf59f5dff16f59ca0720e7c02d820c56b4397c11db5d3eb9", size = 15019, upload-time = "2025-08-19T17:20:56.204Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e0/8ca916dd918018352d377f1f5226ee071cfbeb7dbbde2b03d14a411ac2b1/time_machine-2.19.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e312c7d5d6bfffb96c6a7b39ff29e3046de100d7efaa3c01552654cfbd08f14c", size = 33079, upload-time = "2025-08-19T17:20:57.166Z" }, + { url = "https://files.pythonhosted.org/packages/48/69/184a0209f02dd0cb5e01e8d13cd4c97a5f389c4e3d09b95160dd676ad1e7/time_machine-2.19.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:714c40b2c90d1c57cc403382d5a9cf16e504cb525bfe9650095317da3c3d62b5", size = 34925, upload-time = "2025-08-19T17:20:58.117Z" }, + { url = "https://files.pythonhosted.org/packages/43/42/4bbf4309e8e57cea1086eb99052d97ff6ddecc1ab6a3b07aa4512f8bf963/time_machine-2.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eaa1c675d500dc3ccae19e9fb1feff84458a68c132bbea47a80cc3dd2df7072", size = 36384, upload-time = "2025-08-19T17:20:59.108Z" }, + { url = "https://files.pythonhosted.org/packages/b1/af/9f510dc1719157348c1a2e87423aed406589070b54b503cb237d9bf3a4fe/time_machine-2.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e77a414e9597988af53b2b2e67242c9d2f409769df0d264b6d06fda8ca3360d4", size = 34881, upload-time = "2025-08-19T17:21:00.116Z" }, + { url = "https://files.pythonhosted.org/packages/ca/28/61764a635c70cc76c76ba582dfdc1a84834cddaeb96789023af5214426b2/time_machine-2.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd93996970e11c382b04d4937c3cd0b0167adeef14725ece35aae88d8a01733c", size = 32931, upload-time = "2025-08-19T17:21:01.095Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e0/f028d93b266e6ade8aca5851f76ebbc605b2905cdc29981a2943b43e1a6c/time_machine-2.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8e20a6d8d6e23174bd7e931e134d9610b136db460b249d07e84ecdad029ec352", size = 34241, upload-time = "2025-08-19T17:21:02.052Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a6/36d1950ed1d3f613158024cf1dcc73db1d9ef0b9117cf51ef2e37dc06499/time_machine-2.19.0-cp312-cp312-win32.whl", hash = "sha256:95afc9bc65228b27be80c2756799c20b8eb97c4ef382a9b762b6d7888bc84099", size = 17021, upload-time = "2025-08-19T17:21:03.374Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0d/e2dce93355abda3cac69e77fe96566757e98b8fe7fdcbddce89c9ced3f5f/time_machine-2.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84909af950e2448f4e2562ea5759c946248c99ab380d2b47d79b62bd76fa236", size = 17857, upload-time = "2025-08-19T17:21:04.331Z" }, + { url = "https://files.pythonhosted.org/packages/eb/28/50ae6fb83b7feeeca7a461c0dc156cf7ef5e6ef594a600d06634fde6a2cb/time_machine-2.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:0390a1ea9fa7e9d772a39b7c61b34fdcca80eb9ffac339cc0441c6c714c81470", size = 16677, upload-time = "2025-08-19T17:21:05.39Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b8/24ebce67aa531bae2cbe164bb3f4abc6467dc31f3aead35e77f5a075ea3e/time_machine-2.19.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5e172866753e6041d3b29f3037dc47c20525176a494a71bbd0998dfdc4f11f2f", size = 19373, upload-time = "2025-08-19T17:21:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/c9a5240fd2f845d3ff9fa26f8c8eaa29f7239af9d65007e61d212250f15b/time_machine-2.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f70f68379bd6f542ae6775cce9a4fa3dcc20bf7959c42eaef871c14469e18097", size = 15056, upload-time = "2025-08-19T17:21:07.667Z" }, + { url = "https://files.pythonhosted.org/packages/b9/92/66cce5d2fb2a5e68459aca85fd18a7e2d216f725988940cd83f96630f2f1/time_machine-2.19.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e69e0b0f694728a00e72891ef8dd00c7542952cb1c87237db594b6b27d504a96", size = 33172, upload-time = "2025-08-19T17:21:08.619Z" }, + { url = "https://files.pythonhosted.org/packages/ae/20/b499e9ab4364cd466016c33dcdf4f56629ca4c20b865bd4196d229f31d92/time_machine-2.19.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3ae0a8b869574301ec5637e32c270c7384cca5cd6e230f07af9d29271a7fa293", size = 35042, upload-time = "2025-08-19T17:21:09.622Z" }, + { url = "https://files.pythonhosted.org/packages/41/32/b252d3d32791eb16c07d553c820dbc33d9c7fa771de3d1c602190bded2b7/time_machine-2.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:554e4317de90e2f7605ff80d153c8bb56b38c0d0c0279feb17e799521e987b8c", size = 36535, upload-time = "2025-08-19T17:21:10.571Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/4d0470062b9742e1b040ab81bad04d1a5d1de09806507bb6188989cfa1a7/time_machine-2.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6567a5ec5538ed550539ac29be11b3cb36af1f9894e2a72940cba0292cc7c3c9", size = 34945, upload-time = "2025-08-19T17:21:11.538Z" }, + { url = "https://files.pythonhosted.org/packages/24/71/2f741b29d98b1c18f6777a32236497c3d3264b6077e431cea4695684c8a1/time_machine-2.19.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82e9ffe8dfff07b0d810a2ad015a82cd78c6a237f6c7cf185fa7f747a3256f8a", size = 33014, upload-time = "2025-08-19T17:21:12.858Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/ca8dba6106562843fd99f672e5aaf95badbc10f4f13f7cfe8d8640a7019d/time_machine-2.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e1c4e578cdd69b3531d8dd3fbcb92a0cd879dadb912ee37af99c3a9e3c0d285", size = 34350, upload-time = "2025-08-19T17:21:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/21/7f/34fe540450e18d0a993240100e4b86e8d03d831b92af8bb6ddb2662dc6fc/time_machine-2.19.0-cp313-cp313-win32.whl", hash = "sha256:72dbd4cbc3d96dec9dd281ddfbb513982102776b63e4e039f83afb244802a9e5", size = 17047, upload-time = "2025-08-19T17:21:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5d/c8be73df82c7ebe7cd133279670e89b8b110af3ce1412c551caa9d08e625/time_machine-2.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:e17e3e089ac95f9a145ce07ff615e3c85674f7de36f2d92aaf588493a23ffb4b", size = 17868, upload-time = "2025-08-19T17:21:15.819Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/2dfd3b8fb285308f61cd7aa9bfa96f46ddf916e3549a0f0afd094c556599/time_machine-2.19.0-cp313-cp313-win_arm64.whl", hash = "sha256:149072aff8e3690e14f4916103d898ea0d5d9c95531b6aa0995251c299533f7b", size = 16710, upload-time = "2025-08-19T17:21:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/05/c1/deebb361727d2c5790f9d4d874be1b19afd41f4375581df465e6718b46a2/time_machine-2.19.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f3589fee1ed0ab6ee424a55b0ea1ec694c4ba64cc26895bcd7d99f3d1bc6a28a", size = 20053, upload-time = "2025-08-19T17:21:17.704Z" }, + { url = "https://files.pythonhosted.org/packages/45/e8/fe3376951e6118d8ec1d1f94066a169b791424fe4a26c7dfc069b153ee08/time_machine-2.19.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7887e85275c4975fe54df03dcdd5f38bd36be973adc68a8c77e17441c3b443d6", size = 15423, upload-time = "2025-08-19T17:21:18.668Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/f88d95cd1a87c650cf3749b4d64afdaf580297aa18ad7f4b44ec9d252dfc/time_machine-2.19.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ce0be294c209928563fcce1c587963e60ec803436cf1e181acd5bc1e425d554b", size = 39630, upload-time = "2025-08-19T17:21:19.645Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5d/65a5c48a65357e56ec6f032972e4abd1c02d4fca4b0717a3aaefd19014d4/time_machine-2.19.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a62fd1ab380012c86f4c042010418ed45eb31604f4bf4453e17c9fa60bc56a29", size = 41242, upload-time = "2025-08-19T17:21:20.979Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/fe5209e1615fde0a8cad6c4e857157b150333ed1fe31a7632b08cfe0ebdd/time_machine-2.19.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b25ec853a4530a5800731257f93206b12cbdee85ede964ebf8011b66086a7914", size = 44278, upload-time = "2025-08-19T17:21:21.984Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/a5e5fe9c5d614cde0a9387ff35e8dfd12c5ef6384e4c1a21b04e6e0b905d/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a430e4d0e0556f021a9c78e9b9f68e5e8910bdace4aa34ed4d1a73e239ed9384", size = 42321, upload-time = "2025-08-19T17:21:23.755Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c5/56eca774e9162bc1ce59111d2bd69140dc8908c9478c92ec7bd15d547600/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2415b7495ec4364c8067071e964fbadfe746dd4cdb43983f2f0bd6ebed13315c", size = 39270, upload-time = "2025-08-19T17:21:26.009Z" }, + { url = "https://files.pythonhosted.org/packages/9b/69/5dd0c420667578169a12acc8c8fd7452e8cfb181e41c9b4ac7e88fa36686/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbfc6b90c10f288594e1bf89a728a98cc0030791fd73541bbdc6b090aff83143", size = 40193, upload-time = "2025-08-19T17:21:27.054Z" }, + { url = "https://files.pythonhosted.org/packages/75/a7/de974d421bd55c9355583427c2a38fb0237bb5fd6614af492ba89dacb2f9/time_machine-2.19.0-cp313-cp313t-win32.whl", hash = "sha256:16f5d81f650c0a4d117ab08036dc30b5f8b262e11a4a0becc458e7f1c011b228", size = 17542, upload-time = "2025-08-19T17:21:28.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/aa0d05becd5d06ae8d3f16d657dc8cc9400c8d79aef80299de196467ff12/time_machine-2.19.0-cp313-cp313t-win_amd64.whl", hash = "sha256:645699616ec14e147094f601e6ab9553ff6cea37fad9c42720a6d7ed04bcd5dc", size = 18703, upload-time = "2025-08-19T17:21:29.663Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c0/f785a4c7c73aa176510f7c48b84b49c26be84af0d534deb222e0327f750e/time_machine-2.19.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b32daa965d13237536ea3afaa5ad61ade2b2d9314bc3a20196a0d2e1d7b57c6a", size = 17020, upload-time = "2025-08-19T17:21:30.653Z" }, + { url = "https://files.pythonhosted.org/packages/ed/97/c5fb51def06c0b2b6735332ad118ab35b4d9b85368792e5b638e99b1b686/time_machine-2.19.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:31cb43c8fd2d961f31bed0ff4e0026964d2b35e5de9e0fabbfecf756906d3612", size = 19360, upload-time = "2025-08-19T17:21:31.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4e/2d795f7d6b7f5205ffe737a05bb1cf19d8038233b797062b2ef412b8512b/time_machine-2.19.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bdf481a75afc6bff3e520db594501975b652f7def21cd1de6aa971d35ba644e6", size = 15033, upload-time = "2025-08-19T17:21:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/dd/32/9bad501e360b4e758c58fae616ca5f8c7ad974b343f2463a15b2bf77a366/time_machine-2.19.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:00bee4bb950ac6a08d62af78e4da0cf2b4fc2abf0de2320d0431bf610db06e7c", size = 33379, upload-time = "2025-08-19T17:21:33.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/45/eda0ca4d793dfd162478d6163759b1c6ce7f6e61daa7fd7d62b31f21f87f/time_machine-2.19.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f02199490906582302ce09edd32394fb393271674c75d7aa76c7a3245f16003", size = 35123, upload-time = "2025-08-19T17:21:34.945Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/97e16325442ae5731fcaac794f0a1ef9980eff8a5491e58201d7eb814a34/time_machine-2.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e35726c7ba625f844c13b1fc0d4f81f394eefaee1d3a094a9093251521f2ef15", size = 36588, upload-time = "2025-08-19T17:21:35.975Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9d/bf0b2ccc930cc4a316f26f1c78d3f313cd0fa13bb7480369b730a8f129db/time_machine-2.19.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:304315023999cd401ff02698870932b893369e1cfeb2248d09f6490507a92e97", size = 35013, upload-time = "2025-08-19T17:21:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/39ac6a3078174f9715d88364871348b249631f12e76de1b862433b3f8862/time_machine-2.19.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9765d4f003f263ea8bfd90d2d15447ca4b3dfa181922cf6cf808923b02ac180a", size = 33303, upload-time = "2025-08-19T17:21:38.352Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ac/d8646baf9f95f2e792a6d7a7b35e92fca253c4a992afff801beafae0e5c2/time_machine-2.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7837ef3fd5911eb9b480909bb93d922737b6bdecea99dfcedb0a03807de9b2d3", size = 34440, upload-time = "2025-08-19T17:21:39.382Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/8b6568c5ae966d80ead03ab537be3c6acf2af06fb501c2d466a3162c6295/time_machine-2.19.0-cp314-cp314-win32.whl", hash = "sha256:4bb5bd43b1bdfac3007b920b51d8e761f024ed465cfeec63ac4296922a4ec428", size = 17162, upload-time = "2025-08-19T17:21:40.381Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/211c1ab4566eba5308b2dc001b6349e3a032e3f6afa67ca2f27ea6b27af5/time_machine-2.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:f583bbd0aa8ab4a7c45a684bf636d9e042d466e30bcbae1d13e7541e2cbe7207", size = 18040, upload-time = "2025-08-19T17:21:41.363Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fc/4c2fb705f6371cb83824da45a8b967514a922fc092a0ef53979334d97a70/time_machine-2.19.0-cp314-cp314-win_arm64.whl", hash = "sha256:f379c6f8a6575a8284592179cf528ce89373f060301323edcc44f1fa1d37be12", size = 16752, upload-time = "2025-08-19T17:21:42.336Z" }, + { url = "https://files.pythonhosted.org/packages/79/ab/6437d18f31c666b5116c97572a282ac2590a82a0a9867746a6647eaf4613/time_machine-2.19.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a3b8981f9c663b0906b05ab4d0ca211fae4b63b47c6ec26de5374fe56c836162", size = 20057, upload-time = "2025-08-19T17:21:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a2/e03639ec2ba7200328bbcad8a2b2b1d5fccca9cceb9481b164a1cabdcb33/time_machine-2.19.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e9c6363893e7f52c226afbebb23e825259222d100e67dfd24c8a6d35f1a1907", size = 15430, upload-time = "2025-08-19T17:21:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ff/39e63a48e840f3e36ce24846ee51dd99c6dba635659b1750a2993771e88e/time_machine-2.19.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:206fcd6c9a6f00cac83db446ad1effc530a8cec244d2780af62db3a2d0a9871b", size = 39622, upload-time = "2025-08-19T17:21:45.821Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2e/ee5ac79c4954768705801e54817c7d58e07e25a0bb227e775f501f3e2122/time_machine-2.19.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf33016a1403c123373ffaeff25e26e69d63bf2c63b6163932efed94160db7ef", size = 41235, upload-time = "2025-08-19T17:21:46.783Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3e/9af5f39525e779185c77285b8bbae15340eeeaa0afb33d458bc8b47d459b/time_machine-2.19.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9247c4bb9bbd3ff584ef4efbdec8efd9f37aa08bcfc4728bde1e489c2cb445bd", size = 44276, upload-time = "2025-08-19T17:21:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/572c7443cc27140bbeae3947279bbd4a120f9e8622253a20637f260b7813/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:77f9bb0b86758d1f2d9352642c874946ad5815df53ef4ca22eb9d532179fe50d", size = 42330, upload-time = "2025-08-19T17:21:48.881Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/1a81c2e08ee7dae13ec8ceed27a29afa980c3d63852e42f1e023bf0faa03/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0b529e262df3b9c449f427385f4d98250828c879168c2e00eec844439f40b370", size = 39281, upload-time = "2025-08-19T17:21:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/60/6f0d6e5108978ca1a2a4ffb4d1c7e176d9199bb109fd44efe2680c60b52a/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9199246e31cdc810e5d89cb71d09144c4d745960fdb0824da4994d152aca3303", size = 40201, upload-time = "2025-08-19T17:21:50.953Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/3ea4951e8293b0643feb98c0b9a176fa822154f1810835db3f282968ab10/time_machine-2.19.0-cp314-cp314t-win32.whl", hash = "sha256:0fe81bae55b7aefc2c2a34eb552aa82e6c61a86b3353a3c70df79b9698cb02ca", size = 17743, upload-time = "2025-08-19T17:21:51.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8b/cd802884ca8a98e2b6cdc2397d57dd12ff8a7d1481e06fc3fad3d4e7e5ff/time_machine-2.19.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7253791b8d7e7399fbeed7a8193cb01bc004242864306288797056badbdaf80b", size = 18956, upload-time = "2025-08-19T17:21:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/c6/49/cabb1593896082fd55e34768029b8b0ca23c9be8b2dc127e0fc14796d33e/time_machine-2.19.0-cp314-cp314t-win_arm64.whl", hash = "sha256:536bd1ac31ab06a1522e7bf287602188f502dc19d122b1502c4f60b1e8efac79", size = 17068, upload-time = "2025-08-19T17:21:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/d6/05/0608376c3167afe6cf7cdfd2b05c142ea4c42616eee9ba06d1799965806a/time_machine-2.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8bb00b30ec9fe56d01e9812df1ffe39f331437cef9bfaebcc81c83f7f8f8ee2", size = 19659, upload-time = "2025-08-19T17:21:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/11/c4/72eb8c7b36830cf36c51d7bc2f1ac313d68881c3a58040fb6b42c4523d20/time_machine-2.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d821c60efc08a97cc11e5482798e6fd5eba5c0f22a02db246b50895dbdc0de41", size = 15153, upload-time = "2025-08-19T17:21:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/89/1a/0782e1f5c8ab8809ebd992709e1bb69d67600191baa023af7a5d32023a3c/time_machine-2.19.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb051aec7b3b6e96a200d911c225901e6133ff3da11e470e24111a53bbc13637", size = 32555, upload-time = "2025-08-19T17:21:57.74Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/8ef58e2f6321851d5900ca3d18044938832c2ed42a2ac7570ca6aa29768a/time_machine-2.19.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe59909d95a2ef5e01ce3354fdea3908404c2932c2069f00f66dff6f27e9363e", size = 34185, upload-time = "2025-08-19T17:21:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/82/74/ce0c9867f788c1fb22c417ec1aae47a24117e53d51f6ff97d7c6ca5392f6/time_machine-2.19.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29e84b8682645b16eb6f9e8ec11c35324ad091841a11cf4fc3fc7f6119094c89", size = 35917, upload-time = "2025-08-19T17:22:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/d2/70/6f97a8f552dbaa66feb10170b5726dab74bc531673d1ed9d6f271547e54c/time_machine-2.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a11f1c0e0d06023dc01614c964e256138913551d3ae6dca5148f79081156336", size = 34584, upload-time = "2025-08-19T17:22:01.447Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/cf139088ce537c15d7f03cf56ec317d3a5cfb520e30aa711ea0248d0ae8a/time_machine-2.19.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:57a235a6307c54df50e69f1906e2f199e47da91bde4b886ee05aff57fe4b6bf6", size = 32608, upload-time = "2025-08-19T17:22:02.548Z" }, + { url = "https://files.pythonhosted.org/packages/b1/17/0ec41ef7a30c6753fb226a28b74162b264b35724905ced4098f2f5076ded/time_machine-2.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:426aba552f7af9604adad9ef570c859af7c1081d878db78089fac159cd911b0a", size = 33686, upload-time = "2025-08-19T17:22:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/586f15159083ec84f178d494c60758c46603b00c9641b04deb63f1950128/time_machine-2.19.0-cp39-cp39-win32.whl", hash = "sha256:67772c7197a3a712d1b970ed545c6e98db73524bd90e245fd3c8fa7ad7630768", size = 17133, upload-time = "2025-08-19T17:22:04.989Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/bfe4b906a9fe0bf2d011534314212ed752d6b8f392c9c82f6ac63dccc5ab/time_machine-2.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:011d7859089263204dc5fdf83dce7388f986fe833c9381d6106b4edfda2ebd3e", size = 17972, upload-time = "2025-08-19T17:22:06.026Z" }, + { url = "https://files.pythonhosted.org/packages/5d/73/182343eba05aa5787732aaa68f3b3feb5e40ddf86b928ae941be45646393/time_machine-2.19.0-cp39-cp39-win_arm64.whl", hash = "sha256:e1af66550fa4685434f00002808a525f176f1f92746646c0019bb86fbff48b27", size = 16820, upload-time = "2025-08-19T17:22:07.227Z" }, +] + +[[package]] +name = "time-machine" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/02/fc/37b02f6094dbb1f851145330460532176ed2f1dc70511a35828166c41e52/time_machine-3.2.0.tar.gz", hash = "sha256:a4ddd1cea17b8950e462d1805a42b20c81eb9aafc8f66b392dd5ce997e037d79", size = 14804, upload-time = "2025-12-17T23:33:02.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/31/6bf41cb4a326230518d9b76c910dfc11d4fc23444d1cbfdf2d7652bd99f4/time_machine-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:68142c070e78b62215d8029ec7394905083a4f9aacb0a2a11514ce70b5951b13", size = 19447, upload-time = "2025-12-17T23:31:30.181Z" }, + { url = "https://files.pythonhosted.org/packages/fa/14/d71ce771712e1cbfa15d8c24452225109262b16cb6caaf967e9f60662b67/time_machine-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:161bbd0648802ffdfcb4bb297ecb26b3009684a47d3a4dedb90bc549df4fa2ad", size = 15432, upload-time = "2025-12-17T23:31:31.381Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/dcb43a11f8029561996fad58ff9d3dc5e6d7f32b74f0745a2965d7e4b4f3/time_machine-3.2.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1359ba8c258be695ba69253bc84db882fd616fe69b426cc6056536da2c7bf68e", size = 32956, upload-time = "2025-12-17T23:31:32.469Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/d802cd3c335c414f9b11b479f7459aa72df5de6485c799966cfdf8856d53/time_machine-3.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c85b169998ca2c24a78fb214586ec11c4cad56d9c38f55ad8326235cb481c884", size = 34556, upload-time = "2025-12-17T23:31:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/85/ee/51ad553514ab0b940c7c82c6e1519dd10fd06ac07b32039a1d153ef09c88/time_machine-3.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65b9367cb8a10505bc8f67da0da514ba20fa816fc47e11f434f7c60350322b4c", size = 36101, upload-time = "2025-12-17T23:31:35.462Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/938b111b5bb85a2b07502d0f9d8a704fc75bd760d62e76bce23c89ed16c9/time_machine-3.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9faca6a0f1973d7df3233c951fc2a11ff0c54df74087d8aaf41ae3deb19d0893", size = 34905, upload-time = "2025-12-17T23:31:36.543Z" }, + { url = "https://files.pythonhosted.org/packages/dd/50/0951f73b23e76455de0b4a3a58ac5a24bd8d10489624b1c5e03f10c6fc0b/time_machine-3.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:213b1ada7f385d467e598999b642eda4a8e89ae10ad5dc4f5d8f672cbf604261", size = 33012, upload-time = "2025-12-17T23:31:37.967Z" }, + { url = "https://files.pythonhosted.org/packages/4f/95/5304912d3dcecc4e14ed222dbe0396352efdf8497534abc3c9edd67a7528/time_machine-3.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:160b6afd94c39855af04d39c58e4cf602406abd6d79427ab80e830ea71789cfb", size = 34104, upload-time = "2025-12-17T23:31:39.449Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/af56518652ec7adac4ced193b7a42c4ff354fef28a412b3b5ffa5763aead/time_machine-3.2.0-cp310-cp310-win32.whl", hash = "sha256:c15d9ac257c78c124d112e4fc91fa9f3dcb004bdda913c19f0e7368d713cf080", size = 17468, upload-time = "2025-12-17T23:31:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/0213f00ca3cf6fe1c9fdbd7fd467e801052fc85534f30c0e4684bd474190/time_machine-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:3bf0f428487f93b8fe9d27aa01eccc817885da3290b467341b4a4a795e1d1891", size = 18313, upload-time = "2025-12-17T23:31:41.617Z" }, + { url = "https://files.pythonhosted.org/packages/77/e4/811f96aa7a634b2b264d9a476f3400e710744dda503b4ad87a5c76db32c9/time_machine-3.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:347f6be2129fcd35b1c94b9387fcb2cbe7949b1e649228c5f22949a811b78976", size = 17037, upload-time = "2025-12-17T23:31:42.924Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e1/03aae5fbaa53859f665094af696338fc7cae733d926a024af69982712350/time_machine-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c188a9dda9fcf975022f1b325b466651b96a4dfc223c523ed7ed8d979f9bf3e8", size = 19143, upload-time = "2025-12-17T23:31:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/98cb17bebb52b22ff4ec26984dd44280f9c71353c3bae0640a470e6683e5/time_machine-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17245f1cc2dd13f9d63a174be59bb2684a9e5e0a112ab707e37be92068cd655f", size = 15273, upload-time = "2025-12-17T23:31:45.246Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2f/ca11e4a7897234bb9331fcc5f4ed4714481ba4012370cc79a0ae8c42ea0a/time_machine-3.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9bd1de1996e76efd36ae15970206c5089fb3728356794455bd5cd8d392b5537", size = 31049, upload-time = "2025-12-17T23:31:46.613Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/d17d83a59943094e6b6c6a3743caaf6811b12203c3e07a30cc7bcc2ab7ee/time_machine-3.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98493cd50e8b7f941eab69b9e18e697ad69db1a0ec1959f78f3d7b0387107e5c", size = 32632, upload-time = "2025-12-17T23:31:47.72Z" }, + { url = "https://files.pythonhosted.org/packages/71/50/d60576d047a0dfb5638cdfb335e9c3deb6e8528544fa0b3966a8480f72b7/time_machine-3.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31f2a33d595d9f91eb9bc7f157f0dc5721f5789f4c4a9e8b852cdedb2a7d9b16", size = 34289, upload-time = "2025-12-17T23:31:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/fa/fe/4afa602dbdebddde6d0ea4a7fe849e49b9bb85dc3fb415725a87ccb4b471/time_machine-3.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9f78ac4213c10fbc44283edd1a29cfb7d3382484f4361783ddc057292aaa1889", size = 33175, upload-time = "2025-12-17T23:31:50.611Z" }, + { url = "https://files.pythonhosted.org/packages/0d/87/c152e23977c1d7d7c94eb3ed3ea45cc55971796205125c6fdff40db2c60f/time_machine-3.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c1326b09e947b360926d529a96d1d9e126ce120359b63b506ecdc6ee20755c23", size = 31170, upload-time = "2025-12-17T23:31:51.645Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/54acf51d0f3ade3b51eab73df6192937c9a938753ef5456dff65eb8630be/time_machine-3.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9f2949f03d15264cc15c38918a2cda8966001f0f4ebe190cbfd9c56d91aed8ac", size = 32292, upload-time = "2025-12-17T23:31:52.803Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bc/3745963f36e75661a807196428639327a366f4332f35f1f775c074d4062f/time_machine-3.2.0-cp311-cp311-win32.whl", hash = "sha256:6dfe48e0499e6e16751476b9799e67be7514e6ef04cdf39571ef95a279645831", size = 17349, upload-time = "2025-12-17T23:31:54.19Z" }, + { url = "https://files.pythonhosted.org/packages/82/a2/057469232a99d1f5a0160ae7c5bae7b095c9168b333dd598fcbcfbc1c87b/time_machine-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:809bdf267a29189c304154873620fe0bcc0c9513295fa46b19e21658231c4915", size = 18191, upload-time = "2025-12-17T23:31:55.472Z" }, + { url = "https://files.pythonhosted.org/packages/79/d8/bf9c8de57262ee7130d92a6ed49ed6a6e40a36317e46979428d373630c12/time_machine-3.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:a3f4c17fa90f54902a3f8692c75caf67be87edc3429eeb71cb4595da58198f8e", size = 16905, upload-time = "2025-12-17T23:31:56.658Z" }, + { url = "https://files.pythonhosted.org/packages/71/8b/080c8eedcd67921a52ba5bd0e075362062509ab63c86fc1a0442fad241a6/time_machine-3.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc4bee5b0214d7dc4ebc91f4a4c600f1a598e9b5606ac751f42cb6f6740b1dbb", size = 19255, upload-time = "2025-12-17T23:31:58.057Z" }, + { url = "https://files.pythonhosted.org/packages/66/17/0e5291e9eb705bf8a5a1305f826e979af307bbeb79def4ddbf4b3f9a81e0/time_machine-3.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ca036304b4460ae2fdc1b52dd8b1fa7cf1464daa427fc49567413c09aa839c1", size = 15360, upload-time = "2025-12-17T23:31:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/9ab87b71d2e2b62463b9b058b7ae7ac09fb57f8fcd88729dec169d304340/time_machine-3.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5442735b41d7a2abc2f04579b4ca6047ed4698a8338a4fec92c7c9423e7938cb", size = 33029, upload-time = "2025-12-17T23:32:00.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/26/b5ca19da6f25ea905b3e10a0ea95d697c1aeba0404803a43c68f1af253e6/time_machine-3.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:97da3e971e505cb637079fb07ab0bcd36e33279f8ecac888ff131f45ef1e4d8d", size = 34579, upload-time = "2025-12-17T23:32:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/79/ca/6ac7ad5f10ea18cc1d9de49716ba38c32132c7b64532430d92ef240c116b/time_machine-3.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3cdda6dee4966e38aeb487309bb414c6cb23a81fc500291c77a8fcd3098832e7", size = 35961, upload-time = "2025-12-17T23:32:02.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/390dd958bed395ab32d79a9fe61fe111825c0dd4ded54dbba7e867f171e6/time_machine-3.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:33d9efd302a6998bcc8baa4d84f259f8a4081105bd3d7f7af7f1d0abd3b1c8aa", size = 34668, upload-time = "2025-12-17T23:32:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/da/57/c88fff034a4e9538b3ae7c68c9cfb283670b14d17522c5a8bc17d29f9a4b/time_machine-3.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3a0b0a33971f14145853c9bd95a6ab0353cf7e0019fa2a7aa1ae9fddfe8eab50", size = 32891, upload-time = "2025-12-17T23:32:04.656Z" }, + { url = "https://files.pythonhosted.org/packages/2d/70/ebbb76022dba0fec8f9156540fc647e4beae1680c787c01b1b6200e56d70/time_machine-3.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2d0be9e5f22c38082d247a2cdcd8a936504e9db60b7b3606855fb39f299e9548", size = 34080, upload-time = "2025-12-17T23:32:06.146Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/2ca9e7af3df540dc1c79e3de588adeddb7dcc2107829248e6969c4f14167/time_machine-3.2.0-cp312-cp312-win32.whl", hash = "sha256:3f74623648b936fdce5f911caf386c0a0b579456410975de8c0dfeaaffece1d8", size = 17371, upload-time = "2025-12-17T23:32:07.164Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ce/21d23efc9c2151939af1b7ee4e60d86d661b74ef32b8eaa148f6fe8c899c/time_machine-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:34e26a41d994b5e4b205136a90e9578470386749cc9a2ecf51ca18f83ce25e23", size = 18132, upload-time = "2025-12-17T23:32:08.447Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/c2b70be483accf6db9e5d6c3139bce3c38fe51f898ccf64e8d3fe14fbf4d/time_machine-3.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:0615d3d82c418d6293f271c348945c5091a71f37e37173653d5c26d0e74b13a8", size = 16930, upload-time = "2025-12-17T23:32:09.477Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cd/43ad5efc88298af3c59b66769cea7f055567a85071579ed40536188530c1/time_machine-3.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c421a8eb85a4418a7675a41bf8660224318c46cc62e4751c8f1ceca752059090", size = 19318, upload-time = "2025-12-17T23:32:10.518Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f6/084010ef7f4a3f38b5a4900923d7c85b29e797655c4f6ee4ce54d903cca8/time_machine-3.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4e758f7727d0058c4950c66b58200c187072122d6f7a98b610530a4233ea7b", size = 15390, upload-time = "2025-12-17T23:32:11.625Z" }, + { url = "https://files.pythonhosted.org/packages/25/aa/1cabb74134f492270dc6860cb7865859bf40ecf828be65972827646e91ad/time_machine-3.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:154bd3f75c81f70218b2585cc12b60762fb2665c507eec5ec5037d8756d9b4e0", size = 33115, upload-time = "2025-12-17T23:32:13.219Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/78c5d7dfa366924eb4dbfcc3fc917c39a4280ca234b12819cc1f16c03d88/time_machine-3.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50cfe5ebea422c896ad8d278af9648412b7533b8ea6adeeee698a3fd9b1d3b7", size = 34705, upload-time = "2025-12-17T23:32:14.29Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/d5e877c24541f674c6869ff6e9c56833369796010190252e92c9d7ae5f0f/time_machine-3.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636576501724bd6a9124e69d86e5aef263479e89ef739c5db361469f0463a0a1", size = 36104, upload-time = "2025-12-17T23:32:15.354Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/d4bae72f388f67efc9609f89b012e434bb19d9549c7a7b47d6c7d9e5c55d/time_machine-3.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40e6f40c57197fcf7ec32d2c563f4df0a82c42cdcc3cab27f688e98f6060df10", size = 34765, upload-time = "2025-12-17T23:32:16.434Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c3/ac378cf301d527d8dfad2f0db6bad0dfb1ab73212eaa56d6b96ee5d9d20b/time_machine-3.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a1bcf0b846bbfc19a79bc19e3fa04d8c7b1e8101c1b70340ffdb689cd801ea53", size = 33010, upload-time = "2025-12-17T23:32:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/06/35/7ce897319accda7a6970b288a9a8c52d25227342a7508505a2b3d235b649/time_machine-3.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae55a56c179f4fe7a62575ad5148b6ed82f6c7e5cf2f9a9ec65f2f5b067db5f5", size = 34185, upload-time = "2025-12-17T23:32:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/bf/28/f922022269749cb02eee2b62919671153c4088994fa955a6b0e50327ff81/time_machine-3.2.0-cp313-cp313-win32.whl", hash = "sha256:a66fe55a107e46916007a391d4030479df8864ec6ad6f6a6528221befc5c886e", size = 17397, upload-time = "2025-12-17T23:32:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/fd87cde397f4a7bea493152f0aca8fd569ec709cad9e0f2ca7011eb8c7f7/time_machine-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:30c9ce57165df913e4f74e285a8ab829ff9b7aa3e5ec0973f88f642b9a7b3d15", size = 18139, upload-time = "2025-12-17T23:32:20.991Z" }, + { url = "https://files.pythonhosted.org/packages/75/81/b8ce58233addc5d7d54d2fabc49dcbc02d79e3f079d150aa1bec3d5275ef/time_machine-3.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:89cad7e179e9bdcc84dcf09efe52af232c4cc7a01b3de868356bbd59d95bd9b8", size = 16964, upload-time = "2025-12-17T23:32:22.075Z" }, + { url = "https://files.pythonhosted.org/packages/67/e7/487f0ba5fe6c58186a5e1af2a118dfa2c160fedb37ef53a7e972d410408e/time_machine-3.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:59d71545e62525a4b85b6de9ab5c02ee3c61110fd7f636139914a2335dcbfc9c", size = 20000, upload-time = "2025-12-17T23:32:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/e1/17/eb2c0054c8d44dd42df84ccd434539249a9c7d0b8eb53f799be2102500ab/time_machine-3.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:999672c621c35362bc28e03ca0c7df21500195540773c25993421fd8d6cc5003", size = 15657, upload-time = "2025-12-17T23:32:24.125Z" }, + { url = "https://files.pythonhosted.org/packages/43/21/93443b5d1dd850f8bb9442e90d817a9033dcce6bfbdd3aabbb9786251c80/time_machine-3.2.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5faf7397f0580c7b9d67288522c8d7863e85f0cffadc0f1fccdb2c3dfce5783e", size = 39216, upload-time = "2025-12-17T23:32:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/18544cf8acc72bb1dc03762231c82ecc259733f4bb6770a7bbe5cd138603/time_machine-3.2.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3dd886ec49f1fa5a00e844f5947e5c0f98ce574750c24b7424c6f77fc1c3e87", size = 40764, upload-time = "2025-12-17T23:32:26.643Z" }, + { url = "https://files.pythonhosted.org/packages/27/f7/9fe9ce2795636a3a7467307af6bdf38bb613ddb701a8a5cd50ec713beb5e/time_machine-3.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0ecd96bc7bbe450acaaabe569d84e81688f1be8ad58d1470e42371d145fb53", size = 43526, upload-time = "2025-12-17T23:32:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/a93e975ba9dec22e87ec92d18c28e67d36bd536f9119ffa439b2892b0c9c/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:158220e946c1c4fb8265773a0282c88c35a7e3bb5d78e3561214e3b3231166f3", size = 41727, upload-time = "2025-12-17T23:32:28.985Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fb/e3633e5a6bbed1c76bb2e9810dabc2f8467532ffcd29b9aed404b473061a/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c1aee29bc54356f248d5d7dfdd131e12ca825e850a08c0ebdb022266d073013", size = 38952, upload-time = "2025-12-17T23:32:30.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/3d/02e9fb2526b3d6b1b45bc8e4d912d95d1cd699d1a3f6df985817d37a0600/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8ed2224f09d25b1c2fc98683613aca12f90f682a427eabb68fc824d27014e4a", size = 39829, upload-time = "2025-12-17T23:32:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/85/c8/c14265212436da8e0814c45463987b3f57de3eca4de023cc2eabb0c62ef3/time_machine-3.2.0-cp313-cp313t-win32.whl", hash = "sha256:3498719f8dab51da76d29a20c1b5e52ee7db083dddf3056af7fa69c1b94e1fe6", size = 17852, upload-time = "2025-12-17T23:32:32.079Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bc/8acb13cf6149f47508097b158a9a8bec9ec4530a70cb406124e8023581f5/time_machine-3.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e0d90bee170b219e1d15e6a58164aa808f5170090e4f090bd0670303e34181b1", size = 18918, upload-time = "2025-12-17T23:32:33.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/c443ee508c2708fd2514ccce9052f5e48888783ce690506919629ebc8eb0/time_machine-3.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:051de220fdb6e20d648111bbad423d9506fdbb2e44d4429cef3dc0382abf1fc2", size = 17261, upload-time = "2025-12-17T23:32:34.446Z" }, + { url = "https://files.pythonhosted.org/packages/61/70/b4b980d126ed155c78d1879c50d60c8dcbd47bd11cb14ee7be50e0dfc07f/time_machine-3.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1398980c017fe5744d66f419e0115ee48a53b00b146d738e1416c225eb610b82", size = 19303, upload-time = "2025-12-17T23:32:35.796Z" }, + { url = "https://files.pythonhosted.org/packages/73/73/eaa33603c69a68fe2b6f54f9dd75481693d62f1d29676531002be06e2d1c/time_machine-3.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f8f4e35f4191ef70c2ab8ff490761ee9051b891afce2bf86dde3918eb7b537b", size = 15431, upload-time = "2025-12-17T23:32:37.244Z" }, + { url = "https://files.pythonhosted.org/packages/76/10/b81e138e86cc7bab40cdb59d294b341e172201f4a6c84bb0ec080407977a/time_machine-3.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6db498686ecf6163c5aa8cf0bcd57bbe0f4081184f247edf3ee49a2612b584f9", size = 33206, upload-time = "2025-12-17T23:32:38.713Z" }, + { url = "https://files.pythonhosted.org/packages/d3/72/4deab446b579e8bd5dca91de98595c5d6bd6a17ce162abf5c5f2ce40d3d8/time_machine-3.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:027c1807efb74d0cd58ad16524dec94212fbe900115d70b0123399883657ac0f", size = 34792, upload-time = "2025-12-17T23:32:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/2c/39/439c6b587ddee76d533fe972289d0646e0a5520e14dc83d0a30aeb5565f7/time_machine-3.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92432610c05676edd5e6946a073c6f0c926923123ce7caee1018dc10782c713d", size = 36187, upload-time = "2025-12-17T23:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/4b/db/2da4368db15180989bab83746a857bde05ad16e78f326801c142bb747a06/time_machine-3.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c25586b62480eb77ef3d953fba273209478e1ef49654592cd6a52a68dfe56a67", size = 34855, upload-time = "2025-12-17T23:32:42.817Z" }, + { url = "https://files.pythonhosted.org/packages/88/84/120a431fee50bc4c241425bee4d3a4910df4923b7ab5f7dff1bf0c772f08/time_machine-3.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6bf3a2fa738d15e0b95d14469a0b8ea42635467408d8b490e263d5d45c9a177f", size = 33222, upload-time = "2025-12-17T23:32:43.94Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ea/89cfda82bb8c57ff91bb9a26751aa234d6d90e9b4d5ab0ad9dce0f9f0329/time_machine-3.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ce76b82276d7ad2a66cdc85dad4df19d1422b69183170a34e8fbc4c3f35502f7", size = 34270, upload-time = "2025-12-17T23:32:45.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/aa/235357da4f69a51a8d35fcbfcfa77cdc7dc24f62ae54025006570bda7e2d/time_machine-3.2.0-cp314-cp314-win32.whl", hash = "sha256:14d6778273c543441863dff712cd1d7803dee946b18de35921eb8df10714539d", size = 17544, upload-time = "2025-12-17T23:32:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/6c8405a7276be79693b792cff22ce41067ec05db26a7d02f2d5b06324434/time_machine-3.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbf821da96dbc80d349fa9e7c36e670b41d68a878d28c8850057992fed430eef", size = 18423, upload-time = "2025-12-17T23:32:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/d9/03/a3cf419e20c35fc203c6e4fed48b5b667c1a2b4da456d9971e605f73ecef/time_machine-3.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:71c75d71f8e68abc8b669bca26ed2ddd558430a6c171e32b8620288565f18c0e", size = 17050, upload-time = "2025-12-17T23:32:48.91Z" }, + { url = "https://files.pythonhosted.org/packages/86/a1/142de946dc4393f910bf4564b5c3ba819906e1f49b06c9cb557519c849e4/time_machine-3.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4e374779021446fc2b5c29d80457ec9a3b1a5df043dc2aae07d7c1415d52323c", size = 19991, upload-time = "2025-12-17T23:32:49.933Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/7f17def6289901f94726921811a16b9adce46e666362c75d45730c60274f/time_machine-3.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:122310a6af9c36e9a636da32830e591e7923e8a07bdd0a43276c3a36c6821c90", size = 15707, upload-time = "2025-12-17T23:32:50.969Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d3/3502fb9bd3acb159c18844b26c43220201a0d4a622c0c853785d07699a92/time_machine-3.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba3eeb0f018cc362dd8128befa3426696a2e16dd223c3fb695fde184892d4d8c", size = 39207, upload-time = "2025-12-17T23:32:52.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/be/8b27f4aa296fda14a5a2ad7f588ddd450603c33415ab3f8e85b2f1a44678/time_machine-3.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:77d38ba664b381a7793f8786efc13b5004f0d5f672dae814430445b8202a67a6", size = 40764, upload-time = "2025-12-17T23:32:53.167Z" }, + { url = "https://files.pythonhosted.org/packages/42/cd/fe4c4e5c8ab6d48fab3624c32be9116fb120173a35fe67e482e5cf68b3d2/time_machine-3.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09abeb8f03f044d72712207e0489a62098ad3ad16dac38927fcf80baca4d6a7", size = 43508, upload-time = "2025-12-17T23:32:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/28/5a3ba2fce85b97655a425d6bb20a441550acd2b304c96b2c19d3839f721a/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b28367ce4f73987a55e230e1d30a57a3af85da8eb1a140074eb6e8c7e6ef19f", size = 41712, upload-time = "2025-12-17T23:32:55.781Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/e38084be7fdabb4835db68a3a47e58c34182d79fc35df1ecbe0db2c5359f/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:903c7751c904581da9f7861c3015bed7cdc40047321291d3694a3cdc783bbca3", size = 38939, upload-time = "2025-12-17T23:32:56.867Z" }, + { url = "https://files.pythonhosted.org/packages/40/d0/ad3feb0a392ef4e0c08bc32024950373ddc0669002cbdcbb9f3bf0c2d114/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:528217cad85ede5f85c8bc78b0341868d3c3cfefc6ecb5b622e1cacb6c73247b", size = 39837, upload-time = "2025-12-17T23:32:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/9e/5f4b2ea63b267bd78f3245e76f5528836611b5f2d30b5e7300a722fe4428/time_machine-3.2.0-cp314-cp314t-win32.whl", hash = "sha256:75724762ffd517e7e80aaec1fad1ff5a7414bd84e2b3ee7a0bacfeb67c14926e", size = 18091, upload-time = "2025-12-17T23:32:59.403Z" }, + { url = "https://files.pythonhosted.org/packages/39/6f/456b1f4d2700ae02b19eba830f870596a4b89b74bac3b6c80666f1b108c5/time_machine-3.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2526abbd053c5bca898d1b3e7898eec34626b12206718d8c7ce88fd12c1c9c5c", size = 19208, upload-time = "2025-12-17T23:33:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/8063101427ecd3d2652aada4d21d0876b07a3dc789125bca2ee858fec3ed/time_machine-3.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7f2fb6784b414edbe2c0b558bfaab0c251955ba27edd62946cce4a01675a992c", size = 17359, upload-time = "2025-12-17T23:33:01.54Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/94/fd/6480106702a79bcceda5fd9c63cb19a04a6506bd5ce7fd8d9b63742f0021/yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", size = 141301, upload-time = "2025-10-06T14:12:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/e1/6d95d21b17a93e793e4ec420a925fe1f6a9342338ca7a563ed21129c0990/yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", size = 93864, upload-time = "2025-10-06T14:12:21.05Z" }, + { url = "https://files.pythonhosted.org/packages/32/58/b8055273c203968e89808413ea4c984988b6649baabf10f4522e67c22d2f/yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", size = 94706, upload-time = "2025-10-06T14:12:23.287Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/d7bfbc28a88c2895ecd0da6a874def0c147de78afc52c773c28e1aa233a3/yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", size = 347100, upload-time = "2025-10-06T14:12:28.527Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e8/37a1e7b99721c0564b1fc7b0a4d1f595ef6fb8060d82ca61775b644185f7/yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", size = 318902, upload-time = "2025-10-06T14:12:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ef/34724449d7ef2db4f22df644f2dac0b8a275d20f585e526937b3ae47b02d/yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", size = 363302, upload-time = "2025-10-06T14:12:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/88a39a5dad39889f192cce8d66cc4c58dbeca983e83f9b6bf23822a7ed91/yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", size = 370816, upload-time = "2025-10-06T14:12:34.01Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1f/5e895e547129413f56c76be2c3ce4b96c797d2d0ff3e16a817d9269b12e6/yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", size = 346465, upload-time = "2025-10-06T14:12:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/11/13/a750e9fd6f9cc9ed3a52a70fe58ffe505322f0efe0d48e1fd9ffe53281f5/yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", size = 341506, upload-time = "2025-10-06T14:12:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/67/bb6024de76e7186611ebe626aec5b71a2d2ecf9453e795f2dbd80614784c/yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", size = 335030, upload-time = "2025-10-06T14:12:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/50b38447fd94a7992996a62b8b463d0579323fcfc08c61bdba949eef8a5d/yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", size = 358560, upload-time = "2025-10-06T14:12:41.547Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/c020b6f547578c4e3dbb6335bf918f26e2f34ad0d1e515d72fd33ac0c635/yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", size = 357290, upload-time = "2025-10-06T14:12:43.861Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/c49a619ee35a402fa3a7019a4fa8d26878fec0d1243f6968bbf516789578/yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", size = 350700, upload-time = "2025-10-06T14:12:46.868Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f5042d87777bf6968435f04a2bbb15466b2f142e6e47fa4f34d1a3f32f0c/yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", size = 82323, upload-time = "2025-10-06T14:12:48.633Z" }, + { url = "https://files.pythonhosted.org/packages/fd/58/d00f7cad9eba20c4eefac2682f34661d1d1b3a942fc0092eb60e78cfb733/yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", size = 87145, upload-time = "2025-10-06T14:12:50.241Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/70904f365080780d38b919edd42d224b8c4ce224a86950d2eaa2a24366ad/yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", size = 82173, upload-time = "2025-10-06T14:12:51.869Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 7cb798a68b119343852e08bf56e43fc27970f54a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:32:07 +0000 Subject: [PATCH 045/116] chore: update SDK settings --- .github/workflows/publish-pypi.yml | 28 ++++++++++ .github/workflows/release-doctor.yml | 21 ++++++++ .release-please-manifest.json | 3 ++ .stats.yml | 2 +- CONTRIBUTING.md | 4 +- README.md | 14 ++--- bin/check-release-environment | 21 ++++++++ pyproject.toml | 6 +-- release-please-config.json | 66 +++++++++++++++++++++++ src/cas_parser/_version.py | 2 +- src/cas_parser/resources/access_token.py | 8 +-- src/cas_parser/resources/cams_kfintech.py | 8 +-- src/cas_parser/resources/cdsl/cdsl.py | 8 +-- src/cas_parser/resources/cdsl/fetch.py | 8 +-- src/cas_parser/resources/contract_note.py | 8 +-- src/cas_parser/resources/credits.py | 8 +-- src/cas_parser/resources/inbox.py | 8 +-- src/cas_parser/resources/kfintech.py | 8 +-- src/cas_parser/resources/logs.py | 8 +-- src/cas_parser/resources/nsdl.py | 8 +-- src/cas_parser/resources/smart.py | 8 +-- src/cas_parser/resources/verify_token.py | 8 +-- 22 files changed, 201 insertions(+), 62 deletions(-) create mode 100644 .github/workflows/publish-pypi.yml create mode 100644 .github/workflows/release-doctor.yml create mode 100644 .release-please-manifest.json create mode 100644 bin/check-release-environment create mode 100644 release-please-config.json diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 0000000..9894e82 --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,28 @@ +# This workflow is triggered when a GitHub release is created. +# It can also be run manually to re-publish to PyPI in case it failed for some reason. +# You can run this workflow by navigating to https://www.github.com/CASParser/cas-parser-python/actions/workflows/publish-pypi.yml +name: Publish PyPI +on: + workflow_dispatch: + + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.9.13' + + - name: Publish to PyPI + run: | + bash ./bin/publish-pypi + env: + PYPI_TOKEN: ${{ secrets.CAS_PARSER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..a77924a --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,21 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'CASParser/cas-parser-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v6 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + PYPI_TOKEN: ${{ secrets.CAS_PARSER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..d43a621 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.2.1" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index ff35822..881eb9d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: 267be54ae6400ea329b16189da51ee06 +config_hash: 0147789978cfd5a389e3a322f947cc77 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 13c0870..5274c21 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```sh -$ pip install git+ssh://git@github.com/stainless-sdks/cas-parser-python.git +$ pip install git+ssh://git@github.com/CASParser/cas-parser-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -120,7 +120,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/stainless-sdks/cas-parser-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/CASParser/cas-parser-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index 21886e4..835fc89 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ The full API of this library can be found in [api.md](api.md). ## Installation ```sh -# install from this staging repo -pip install git+ssh://git@github.com/stainless-sdks/cas-parser-python.git +# install from the production repo +pip install git+ssh://git@github.com/CASParser/cas-parser-python.git ``` > [!NOTE] @@ -79,8 +79,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from this staging repo -pip install 'cas_parser[aiohttp] @ git+ssh://git@github.com/stainless-sdks/cas-parser-python.git' +# install from the production repo +pip install 'cas_parser[aiohttp] @ git+ssh://git@github.com/CASParser/cas-parser-python.git' ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: @@ -243,9 +243,9 @@ credit = response.parse() # get the object that `credits.check()` would have re print(credit.enabled_features) ``` -These methods return an [`APIResponse`](https://github.com/stainless-sdks/cas-parser-python/tree/main/src/cas_parser/_response.py) object. +These methods return an [`APIResponse`](https://github.com/CASParser/cas-parser-python/tree/main/src/cas_parser/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/stainless-sdks/cas-parser-python/tree/main/src/cas_parser/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/CASParser/cas-parser-python/tree/main/src/cas_parser/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -349,7 +349,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/stainless-sdks/cas-parser-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/CASParser/cas-parser-python/issues) with questions, bugs, or suggestions. ### Determining the installed version diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..b845b0f --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${PYPI_TOKEN}" ]; then + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/pyproject.toml b/pyproject.toml index fe032ab..2f3478f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,8 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/stainless-sdks/cas-parser-python" -Repository = "https://github.com/stainless-sdks/cas-parser-python" +Homepage = "https://github.com/CASParser/cas-parser-python" +Repository = "https://github.com/CASParser/cas-parser-python" [project.optional-dependencies] aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] @@ -112,7 +112,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/stainless-sdks/cas-parser-python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/CASParser/cas-parser-python/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..316db21 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,66 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "python", + "extra-files": [ + "src/cas_parser/_version.py" + ] +} \ No newline at end of file diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index e9d03e0..92a9acc 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.2.1" +__version__ = "1.2.1" # x-release-please-version diff --git a/src/cas_parser/resources/access_token.py b/src/cas_parser/resources/access_token.py index e8adeac..aa92680 100644 --- a/src/cas_parser/resources/access_token.py +++ b/src/cas_parser/resources/access_token.py @@ -28,7 +28,7 @@ def with_raw_response(self) -> AccessTokenResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AccessTokenResourceWithRawResponse(self) @@ -37,7 +37,7 @@ def with_streaming_response(self) -> AccessTokenResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AccessTokenResourceWithStreamingResponse(self) @@ -95,7 +95,7 @@ def with_raw_response(self) -> AsyncAccessTokenResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncAccessTokenResourceWithRawResponse(self) @@ -104,7 +104,7 @@ def with_streaming_response(self) -> AsyncAccessTokenResourceWithStreamingRespon """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncAccessTokenResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/cams_kfintech.py b/src/cas_parser/resources/cams_kfintech.py index 63d7f7d..04ef754 100644 --- a/src/cas_parser/resources/cams_kfintech.py +++ b/src/cas_parser/resources/cams_kfintech.py @@ -30,7 +30,7 @@ def with_raw_response(self) -> CamsKfintechResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return CamsKfintechResourceWithRawResponse(self) @@ -39,7 +39,7 @@ def with_streaming_response(self) -> CamsKfintechResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return CamsKfintechResourceWithStreamingResponse(self) @@ -107,7 +107,7 @@ def with_raw_response(self) -> AsyncCamsKfintechResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncCamsKfintechResourceWithRawResponse(self) @@ -116,7 +116,7 @@ def with_streaming_response(self) -> AsyncCamsKfintechResourceWithStreamingRespo """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncCamsKfintechResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/cdsl/cdsl.py b/src/cas_parser/resources/cdsl/cdsl.py index 9e59381..d3b39ff 100644 --- a/src/cas_parser/resources/cdsl/cdsl.py +++ b/src/cas_parser/resources/cdsl/cdsl.py @@ -42,7 +42,7 @@ def with_raw_response(self) -> CdslResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return CdslResourceWithRawResponse(self) @@ -51,7 +51,7 @@ def with_streaming_response(self) -> CdslResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return CdslResourceWithStreamingResponse(self) @@ -123,7 +123,7 @@ def with_raw_response(self) -> AsyncCdslResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncCdslResourceWithRawResponse(self) @@ -132,7 +132,7 @@ def with_streaming_response(self) -> AsyncCdslResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncCdslResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/cdsl/fetch.py b/src/cas_parser/resources/cdsl/fetch.py index 85b7a1b..abbb6a1 100644 --- a/src/cas_parser/resources/cdsl/fetch.py +++ b/src/cas_parser/resources/cdsl/fetch.py @@ -29,7 +29,7 @@ def with_raw_response(self) -> FetchResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return FetchResourceWithRawResponse(self) @@ -38,7 +38,7 @@ def with_streaming_response(self) -> FetchResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return FetchResourceWithStreamingResponse(self) @@ -155,7 +155,7 @@ def with_raw_response(self) -> AsyncFetchResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncFetchResourceWithRawResponse(self) @@ -164,7 +164,7 @@ def with_streaming_response(self) -> AsyncFetchResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncFetchResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/contract_note.py b/src/cas_parser/resources/contract_note.py index 514e232..5136c68 100644 --- a/src/cas_parser/resources/contract_note.py +++ b/src/cas_parser/resources/contract_note.py @@ -31,7 +31,7 @@ def with_raw_response(self) -> ContractNoteResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return ContractNoteResourceWithRawResponse(self) @@ -40,7 +40,7 @@ def with_streaming_response(self) -> ContractNoteResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return ContractNoteResourceWithStreamingResponse(self) @@ -138,7 +138,7 @@ def with_raw_response(self) -> AsyncContractNoteResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncContractNoteResourceWithRawResponse(self) @@ -147,7 +147,7 @@ def with_streaming_response(self) -> AsyncContractNoteResourceWithStreamingRespo """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncContractNoteResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/credits.py b/src/cas_parser/resources/credits.py index 8e66bc3..4a86474 100644 --- a/src/cas_parser/resources/credits.py +++ b/src/cas_parser/resources/credits.py @@ -26,7 +26,7 @@ def with_raw_response(self) -> CreditsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return CreditsResourceWithRawResponse(self) @@ -35,7 +35,7 @@ def with_streaming_response(self) -> CreditsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return CreditsResourceWithStreamingResponse(self) @@ -76,7 +76,7 @@ def with_raw_response(self) -> AsyncCreditsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncCreditsResourceWithRawResponse(self) @@ -85,7 +85,7 @@ def with_streaming_response(self) -> AsyncCreditsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncCreditsResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/inbox.py b/src/cas_parser/resources/inbox.py index 8ff5a4f..dd8e8c9 100644 --- a/src/cas_parser/resources/inbox.py +++ b/src/cas_parser/resources/inbox.py @@ -35,7 +35,7 @@ def with_raw_response(self) -> InboxResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return InboxResourceWithRawResponse(self) @@ -44,7 +44,7 @@ def with_streaming_response(self) -> InboxResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return InboxResourceWithStreamingResponse(self) @@ -252,7 +252,7 @@ def with_raw_response(self) -> AsyncInboxResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncInboxResourceWithRawResponse(self) @@ -261,7 +261,7 @@ def with_streaming_response(self) -> AsyncInboxResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncInboxResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/kfintech.py b/src/cas_parser/resources/kfintech.py index 11ab9a5..32077c5 100644 --- a/src/cas_parser/resources/kfintech.py +++ b/src/cas_parser/resources/kfintech.py @@ -28,7 +28,7 @@ def with_raw_response(self) -> KfintechResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return KfintechResourceWithRawResponse(self) @@ -37,7 +37,7 @@ def with_streaming_response(self) -> KfintechResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return KfintechResourceWithStreamingResponse(self) @@ -109,7 +109,7 @@ def with_raw_response(self) -> AsyncKfintechResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncKfintechResourceWithRawResponse(self) @@ -118,7 +118,7 @@ def with_streaming_response(self) -> AsyncKfintechResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncKfintechResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/logs.py b/src/cas_parser/resources/logs.py index 9573ff7..bd6ec9b 100644 --- a/src/cas_parser/resources/logs.py +++ b/src/cas_parser/resources/logs.py @@ -32,7 +32,7 @@ def with_raw_response(self) -> LogsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return LogsResourceWithRawResponse(self) @@ -41,7 +41,7 @@ def with_streaming_response(self) -> LogsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return LogsResourceWithStreamingResponse(self) @@ -149,7 +149,7 @@ def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncLogsResourceWithRawResponse(self) @@ -158,7 +158,7 @@ def with_streaming_response(self) -> AsyncLogsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncLogsResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/nsdl.py b/src/cas_parser/resources/nsdl.py index 9a8045f..4911e54 100644 --- a/src/cas_parser/resources/nsdl.py +++ b/src/cas_parser/resources/nsdl.py @@ -30,7 +30,7 @@ def with_raw_response(self) -> NsdlResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return NsdlResourceWithRawResponse(self) @@ -39,7 +39,7 @@ def with_streaming_response(self) -> NsdlResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return NsdlResourceWithStreamingResponse(self) @@ -107,7 +107,7 @@ def with_raw_response(self) -> AsyncNsdlResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncNsdlResourceWithRawResponse(self) @@ -116,7 +116,7 @@ def with_streaming_response(self) -> AsyncNsdlResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncNsdlResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/smart.py b/src/cas_parser/resources/smart.py index 9e1b0bc..41a4b6f 100644 --- a/src/cas_parser/resources/smart.py +++ b/src/cas_parser/resources/smart.py @@ -30,7 +30,7 @@ def with_raw_response(self) -> SmartResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return SmartResourceWithRawResponse(self) @@ -39,7 +39,7 @@ def with_streaming_response(self) -> SmartResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return SmartResourceWithStreamingResponse(self) @@ -108,7 +108,7 @@ def with_raw_response(self) -> AsyncSmartResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncSmartResourceWithRawResponse(self) @@ -117,7 +117,7 @@ def with_streaming_response(self) -> AsyncSmartResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncSmartResourceWithStreamingResponse(self) diff --git a/src/cas_parser/resources/verify_token.py b/src/cas_parser/resources/verify_token.py index ba3eeaa..273eb62 100644 --- a/src/cas_parser/resources/verify_token.py +++ b/src/cas_parser/resources/verify_token.py @@ -26,7 +26,7 @@ def with_raw_response(self) -> VerifyTokenResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return VerifyTokenResourceWithRawResponse(self) @@ -35,7 +35,7 @@ def with_streaming_response(self) -> VerifyTokenResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return VerifyTokenResourceWithStreamingResponse(self) @@ -70,7 +70,7 @@ def with_raw_response(self) -> AsyncVerifyTokenResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers """ return AsyncVerifyTokenResourceWithRawResponse(self) @@ -79,7 +79,7 @@ def with_streaming_response(self) -> AsyncVerifyTokenResourceWithStreamingRespon """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/stainless-sdks/cas-parser-python#with_streaming_response + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response """ return AsyncVerifyTokenResourceWithStreamingResponse(self) From 5bbf0ed023342bbbde2e2b40e10074c7ed225920 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:32:27 +0000 Subject: [PATCH 046/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 881eb9d..ceaa88c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: 0147789978cfd5a389e3a322f947cc77 +config_hash: d809302a8fbd68d89e2f340990780406 From 8c99e75412e0cd782e27d0194e4057ed2490f81e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:32:52 +0000 Subject: [PATCH 047/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ceaa88c..0d67332 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: d809302a8fbd68d89e2f340990780406 +config_hash: b829eba996751b6279970de4dcc343a1 From ccbecb6052d28747b088c884c0aee52274c70b41 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:33:06 +0000 Subject: [PATCH 048/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 0d67332..59965f2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: b829eba996751b6279970de4dcc343a1 +config_hash: ba177c0fa552925abac02f860f0aba70 From 5e2fa63dc8ff32c54cce53815c9f2de20143ee6e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:33:21 +0000 Subject: [PATCH 049/116] chore: update SDK settings --- .stats.yml | 2 +- README.md | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index 59965f2..92a06be 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: ba177c0fa552925abac02f860f0aba70 +config_hash: c9c82d7a2437cc99fc06dd1f92cf76ab diff --git a/README.md b/README.md index 835fc89..f9e5c2c 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,10 @@ The full API of this library can be found in [api.md](api.md). ## Installation ```sh -# install from the production repo -pip install git+ssh://git@github.com/CASParser/cas-parser-python.git +# install from PyPI +pip install cas_parser ``` -> [!NOTE] -> Once this package is [published to PyPI](https://www.stainless.com/docs/guides/publish), this will become: `pip install cas_parser` - ## Usage The full API of this library can be found in [api.md](api.md). @@ -79,8 +76,8 @@ By default, the async client uses `httpx` for HTTP requests. However, for improv You can enable this by installing `aiohttp`: ```sh -# install from the production repo -pip install 'cas_parser[aiohttp] @ git+ssh://git@github.com/CASParser/cas-parser-python.git' +# install from PyPI +pip install cas_parser[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: From a48b1224152f524da167c19552b1f9c17776aaf4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:42:35 +0000 Subject: [PATCH 050/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d43a621..2a8f4ff 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.2.1" + ".": "1.3.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2f3478f..27c0841 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas_parser" -version = "1.2.1" +version = "1.3.0" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 92a9acc..57a16ae 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.2.1" # x-release-please-version +__version__ = "1.3.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index 5ca0453..01d7228 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser" -version = "1.2.1" +version = "1.3.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From 65688c390ab3d69122b30932ed4438c3bcade016 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:43:15 +0000 Subject: [PATCH 051/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 92a06be..5089578 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: c9c82d7a2437cc99fc06dd1f92cf76ab +config_hash: 45c71ec3a79a4740623d68b2319ff2bd From 2255c552bf86c2ab946245588aa22439a9a4fbd3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:43:53 +0000 Subject: [PATCH 052/116] chore: configure new SDK language --- .stats.yml | 2 +- README.md | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 5089578..4d62cd0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: 45c71ec3a79a4740623d68b2319ff2bd +config_hash: 1af2e938c93ea4ec25fc633469072c43 diff --git a/README.md b/README.md index f9e5c2c..9fa493d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,15 @@ and offers both synchronous and asynchronous clients powered by [httpx](https:// It is generated with [Stainless](https://www.stainless.com/). +## MCP Server + +Use the Cas Parser MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. + +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=cas-parser-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNhcy1wYXJzZXItbWNwIl0sImVudiI6eyJDQVNfUEFSU0VSX0FQSV9LRVkiOiJNeSBBUEkgS2V5In19) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22cas-parser-mcp%22%5D%2C%22env%22%3A%7B%22CAS_PARSER_API_KEY%22%3A%22My%20API%20Key%22%7D%7D) + +> Note: You may need to set environment variables in your MCP client. + ## Documentation The full API of this library can be found in [api.md](api.md). From ab3d8c1339e6505c1616fbdc876c3356cd75ca53 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:48:06 +0000 Subject: [PATCH 053/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2a8f4ff..0e5b256 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.3.0" + ".": "1.3.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 27c0841..b2e628f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas_parser" -version = "1.3.0" +version = "1.3.1" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 57a16ae..146a2f3 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.3.0" # x-release-please-version +__version__ = "1.3.1" # x-release-please-version diff --git a/uv.lock b/uv.lock index 01d7228..09bf971 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser" -version = "1.3.0" +version = "1.3.1" source = { editable = "." } dependencies = [ { name = "anyio" }, From 903b2cc1b842dfeccfbe9d31673d82b39b3ce473 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:57:08 +0000 Subject: [PATCH 054/116] chore: update SDK settings --- .stats.yml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4d62cd0..8281628 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: 1af2e938c93ea4ec25fc633469072c43 +config_hash: bb1b61e2661a7ef2d197a0d4012e9e8a diff --git a/README.md b/README.md index 9fa493d..f6b5f5d 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/). Use the Cas Parser MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=cas-parser-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNhcy1wYXJzZXItbWNwIl0sImVudiI6eyJDQVNfUEFSU0VSX0FQSV9LRVkiOiJNeSBBUEkgS2V5In19) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22cas-parser-mcp%22%5D%2C%22env%22%3A%7B%22CAS_PARSER_API_KEY%22%3A%22My%20API%20Key%22%7D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=cas-parser-mcp&config=eyJuYW1lIjoiY2FzLXBhcnNlci1tY3AiLCJ0cmFuc3BvcnQiOiJodHRwIiwidXJsIjoiaHR0cHM6Ly9jYXMtcGFyc2VyLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7IngtYXBpLWtleSI6Ik15IEFQSSBLZXkifX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fcas-parser.stlmcp.com%22%2C%22headers%22%3A%7B%22x-api-key%22%3A%22My%20API%20Key%22%7D%7D) > Note: You may need to set environment variables in your MCP client. From 4689b8fcecb221dedafbda8101997149b27731a8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:57:22 +0000 Subject: [PATCH 055/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 8281628..415d8d1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: bb1b61e2661a7ef2d197a0d4012e9e8a +config_hash: 97b6c66e88343f85b8b173ee4457919f From ec4c9e2462d1be9412b13b6f0be4a331aa717e43 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:58:38 +0000 Subject: [PATCH 056/116] chore: update SDK settings --- .stats.yml | 2 +- README.md | 6 +- pyproject.toml | 2 +- requirements-dev.lock | 12 +-- uv.lock | 170 +++++++++++++++++++++--------------------- 5 files changed, 96 insertions(+), 96 deletions(-) diff --git a/.stats.yml b/.stats.yml index 415d8d1..a41a618 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: 97b6c66e88343f85b8b173ee4457919f +config_hash: 11ccfc363acca397f243709a8a097bb7 diff --git a/README.md b/README.md index f6b5f5d..a7b0a61 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Cas Parser Python API library -[![PyPI version](https://img.shields.io/pypi/v/cas_parser.svg?label=pypi%20(stable))](https://pypi.org/project/cas_parser/) +[![PyPI version](https://img.shields.io/pypi/v/cas-parser-python.svg?label=pypi%20(stable))](https://pypi.org/project/cas-parser-python/) The Cas Parser Python library provides convenient access to the Cas Parser REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, @@ -26,7 +26,7 @@ The full API of this library can be found in [api.md](api.md). ```sh # install from PyPI -pip install cas_parser +pip install cas-parser-python ``` ## Usage @@ -86,7 +86,7 @@ You can enable this by installing `aiohttp`: ```sh # install from PyPI -pip install cas_parser[aiohttp] +pip install cas-parser-python[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: diff --git a/pyproject.toml b/pyproject.toml index b2e628f..6ba765f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "cas_parser" +name = "cas-parser-python" version = "1.3.1" description = "The official Python library for the cas-parser API" dynamic = ["readme"] diff --git a/requirements-dev.lock b/requirements-dev.lock index 9ca2ac3..e5361ce 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -5,7 +5,7 @@ annotated-types==0.7.0 # via pydantic anyio==4.12.1 # via - # cas-parser + # cas-parser-python # httpx backports-asyncio-runner==1.2.0 ; python_full_version < '3.11' # via pytest-asyncio @@ -17,7 +17,7 @@ colorama==0.4.6 ; sys_platform == 'win32' # via pytest dirty-equals==0.11 distro==1.9.0 - # via cas-parser + # via cas-parser-python exceptiongroup==1.3.1 ; python_full_version < '3.11' # via # anyio @@ -30,7 +30,7 @@ httpcore==1.0.9 # via httpx httpx==0.28.1 # via - # cas-parser + # cas-parser-python # respx idna==3.11 # via @@ -59,7 +59,7 @@ pathspec==1.0.3 pluggy==1.6.0 # via pytest pydantic==2.12.5 - # via cas-parser + # via cas-parser-python pydantic-core==2.41.5 # via pydantic pygments==2.19.2 @@ -86,7 +86,7 @@ ruff==0.14.13 six==1.17.0 ; python_full_version < '3.10' # via python-dateutil sniffio==1.3.1 - # via cas-parser + # via cas-parser-python time-machine==2.19.0 ; python_full_version < '3.10' time-machine==3.2.0 ; python_full_version >= '3.10' tomli==2.4.0 ; python_full_version < '3.11' @@ -96,7 +96,7 @@ tomli==2.4.0 ; python_full_version < '3.11' typing-extensions==4.15.0 # via # anyio - # cas-parser + # cas-parser-python # exceptiongroup # mypy # pydantic diff --git a/uv.lock b/uv.lock index 09bf971..f5b2e38 100644 --- a/uv.lock +++ b/uv.lock @@ -2,17 +2,17 @@ version = 1 revision = 3 requires-python = ">=3.9" resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", - "python_full_version < '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version < '3.10' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", ] conflicts = [[ - { package = "cas-parser", group = "pydantic-v1" }, - { package = "cas-parser", group = "pydantic-v2" }, + { package = "cas-parser-python", group = "pydantic-v1" }, + { package = "cas-parser-python", group = "pydantic-v2" }, ]] [[package]] @@ -31,7 +31,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, - { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, @@ -167,7 +167,7 @@ version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ @@ -188,9 +188,9 @@ name = "anyio" version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ @@ -225,15 +225,15 @@ wheels = [ ] [[package]] -name = "cas-parser" +name = "cas-parser-python" version = "1.3.1" source = { editable = "." } dependencies = [ { name = "anyio" }, { name = "distro" }, { name = "httpx" }, - { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-10-cas-parser-pydantic-v1'" }, - { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, + { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-17-cas-parser-python-pydantic-v1'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-17-cas-parser-python-pydantic-v2' or extra != 'group-17-cas-parser-python-pydantic-v1'" }, { name = "sniffio" }, { name = "typing-extensions" }, ] @@ -250,16 +250,16 @@ dev = [ { name = "importlib-metadata" }, { name = "mypy" }, { name = "pyright" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, { name = "pytest-xdist" }, { name = "respx" }, { name = "rich" }, { name = "ruff" }, - { name = "time-machine", version = "2.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "time-machine", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "time-machine", version = "2.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "time-machine", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] pydantic-v1 = [ { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" } }, @@ -342,7 +342,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -583,10 +583,10 @@ name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", ] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ @@ -601,7 +601,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "mdurl", marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ @@ -613,13 +613,13 @@ name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "mdurl", marker = "python_full_version >= '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ @@ -640,7 +640,7 @@ name = "multidict" version = "6.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } wheels = [ @@ -798,7 +798,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } @@ -1019,7 +1019,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "typing-extensions", marker = "extra == 'group-10-cas-parser-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-17-cas-parser-python-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7b/da/fd89f987a376c807cd81ea0eff4589aade783bbb702637b4734ef2c743a2/pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e", size = 357906, upload-time = "2025-12-18T15:47:46.557Z" } wheels = [ @@ -1061,17 +1061,17 @@ name = "pydantic" version = "2.12.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", - "python_full_version < '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", ] dependencies = [ - { name = "annotated-types", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, - { name = "pydantic-core", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, - { name = "typing-extensions", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, - { name = "typing-inspection", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, + { name = "annotated-types", marker = "extra == 'group-17-cas-parser-python-pydantic-v2' or extra != 'group-17-cas-parser-python-pydantic-v1'" }, + { name = "pydantic-core", marker = "extra == 'group-17-cas-parser-python-pydantic-v2' or extra != 'group-17-cas-parser-python-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-17-cas-parser-python-pydantic-v2' or extra != 'group-17-cas-parser-python-pydantic-v1'" }, + { name = "typing-inspection", marker = "extra == 'group-17-cas-parser-python-pydantic-v2' or extra != 'group-17-cas-parser-python-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ @@ -1083,7 +1083,7 @@ name = "pydantic-core" version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-17-cas-parser-python-pydantic-v2' or extra != 'group-17-cas-parser-python-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ @@ -1239,13 +1239,13 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "pluggy", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ @@ -1257,19 +1257,19 @@ name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", ] dependencies = [ - { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "packaging", marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "pluggy", marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "pygments", marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version >= '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version >= '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version >= '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ @@ -1284,9 +1284,9 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "backports-asyncio-runner", marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } wheels = [ @@ -1298,15 +1298,15 @@ name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", ] dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "typing-extensions", marker = "(python_full_version >= '3.10' and python_full_version < '3.13') or (python_full_version < '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2') or (python_full_version >= '3.13' and extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.10' and python_full_version < '3.13') or (python_full_version < '3.10' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2') or (python_full_version >= '3.13' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ @@ -1319,8 +1319,8 @@ version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ @@ -1332,7 +1332,7 @@ name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "six", marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ @@ -1356,8 +1356,8 @@ name = "rich" version = "14.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } @@ -1417,7 +1417,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "python-dateutil", marker = "python_full_version < '3.10' or (extra == 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2')" }, + { name = "python-dateutil", marker = "python_full_version < '3.10' or (extra == 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/1b5fdd165f61b67f445fac2a7feb0c655118edef429cd09ff5a8067f7f1d/time_machine-2.19.0.tar.gz", hash = "sha256:7c5065a8b3f2bbb449422c66ef71d114d3f909c276a6469642ecfffb6a0fcd29", size = 14576, upload-time = "2025-08-19T17:22:08.402Z" } wheels = [ @@ -1516,10 +1516,10 @@ name = "time-machine" version = "3.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-10-cas-parser-pydantic-v1' and extra == 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra == 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", - "python_full_version >= '3.10' and extra != 'group-10-cas-parser-pydantic-v1' and extra != 'group-10-cas-parser-pydantic-v2'", + "python_full_version >= '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra == 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-17-cas-parser-python-pydantic-v1' and extra != 'group-17-cas-parser-python-pydantic-v2'", ] sdist = { url = "https://files.pythonhosted.org/packages/02/fc/37b02f6094dbb1f851145330460532176ed2f1dc70511a35828166c41e52/time_machine-3.2.0.tar.gz", hash = "sha256:a4ddd1cea17b8950e462d1805a42b20c81eb9aafc8f66b392dd5ce997e037d79", size = 14804, upload-time = "2025-12-17T23:33:02.599Z" } wheels = [ @@ -1670,7 +1670,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "extra == 'group-10-cas-parser-pydantic-v2' or extra != 'group-10-cas-parser-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-17-cas-parser-python-pydantic-v2' or extra != 'group-17-cas-parser-python-pydantic-v1'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ From 8c03863fe9aac9afe68471b39559608f6d5ed9c0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:58:54 +0000 Subject: [PATCH 057/116] chore: update SDK settings --- .github/workflows/publish-pypi.yml | 5 +++-- .github/workflows/release-doctor.yml | 2 -- .stats.yml | 2 +- bin/check-release-environment | 4 ---- bin/publish-pypi | 6 +++++- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 9894e82..92d0c62 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -12,6 +12,9 @@ jobs: publish: name: publish runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v6 @@ -24,5 +27,3 @@ jobs: - name: Publish to PyPI run: | bash ./bin/publish-pypi - env: - PYPI_TOKEN: ${{ secrets.CAS_PARSER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index a77924a..c918745 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -17,5 +17,3 @@ jobs: - name: Check release environment run: | bash ./bin/check-release-environment - env: - PYPI_TOKEN: ${{ secrets.CAS_PARSER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.stats.yml b/.stats.yml index a41a618..e7c969a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: 11ccfc363acca397f243709a8a097bb7 +config_hash: e78bb30ba7c06b2a6d20092a5872aec2 diff --git a/bin/check-release-environment b/bin/check-release-environment index b845b0f..1e951e9 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -2,10 +2,6 @@ errors=() -if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") -fi - lenErrors=${#errors[@]} if [[ lenErrors -gt 0 ]]; then diff --git a/bin/publish-pypi b/bin/publish-pypi index e72ca2f..5895700 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -4,4 +4,8 @@ set -eux rm -rf dist mkdir -p dist uv build -uv publish --token=$PYPI_TOKEN +if [ -n "${PYPI_TOKEN:-}" ]; then + uv publish --token=$PYPI_TOKEN +else + uv publish +fi From 6bc0d6f887ac1e22cffb97ccd98d6d7e75ce810f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 01:04:17 +0000 Subject: [PATCH 058/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0e5b256..c658eef 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.3.1" + ".": "1.3.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6ba765f..304c5ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.3.1" +version = "1.3.2" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 146a2f3..d45d1d6 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.3.1" # x-release-please-version +__version__ = "1.3.2" # x-release-please-version diff --git a/uv.lock b/uv.lock index f5b2e38..bdb9b19 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.3.1" +version = "1.3.2" source = { editable = "." } dependencies = [ { name = "anyio" }, From f74127f2cf2c36de1b61dfc53fae5ee91da24a6b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 01:05:45 +0000 Subject: [PATCH 059/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index e7c969a..917f8d3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: e78bb30ba7c06b2a6d20092a5872aec2 +config_hash: 3984d901dd3643875b212ecb24263a9d From a64764369e05303baeeaaeb5c2ee17e536e1083c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 01:31:20 +0000 Subject: [PATCH 060/116] feat(api): manual updates --- .stats.yml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 917f8d3..9dc7546 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: 3984d901dd3643875b212ecb24263a9d +config_hash: 71b56825479262e2a16b9a2fe978c174 diff --git a/README.md b/README.md index a7b0a61..7cb8876 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/). Use the Cas Parser MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=cas-parser-mcp&config=eyJuYW1lIjoiY2FzLXBhcnNlci1tY3AiLCJ0cmFuc3BvcnQiOiJodHRwIiwidXJsIjoiaHR0cHM6Ly9jYXMtcGFyc2VyLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7IngtYXBpLWtleSI6Ik15IEFQSSBLZXkifX0) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fcas-parser.stlmcp.com%22%2C%22headers%22%3A%7B%22x-api-key%22%3A%22My%20API%20Key%22%7D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=cas-parser-node-mcp&config=eyJuYW1lIjoiY2FzLXBhcnNlci1ub2RlLW1jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL2Nhcy1wYXJzZXIuc3RsbWNwLmNvbSIsImhlYWRlcnMiOnsieC1hcGkta2V5IjoiTXkgQVBJIEtleSJ9fQ) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-node-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fcas-parser.stlmcp.com%22%2C%22headers%22%3A%7B%22x-api-key%22%3A%22My%20API%20Key%22%7D%7D) > Note: You may need to set environment variables in your MCP client. From 842cdd604fdad7dc6fa95d37efd228167c4026be Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 01:31:36 +0000 Subject: [PATCH 061/116] feat(api): manual updates --- .stats.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9dc7546..32808d2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: 71b56825479262e2a16b9a2fe978c174 +config_hash: ab495a165f0919b37cbf9efbd0f0e6ef diff --git a/README.md b/README.md index 7cb8876..f7b645a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Use the Cas Parser MCP Server to enable AI assistants to interact with this API, ## Documentation -The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found on [docs.casparser.in](https://docs.casparser.in). The full API of this library can be found in [api.md](api.md). ## Installation From 65406090d2798ef10303ad8afde2d6045c964316 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 01:33:33 +0000 Subject: [PATCH 062/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c658eef..3e9af1b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.3.2" + ".": "1.4.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 304c5ef..54fc544 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.3.2" +version = "1.4.0" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index d45d1d6..682501f 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.3.2" # x-release-please-version +__version__ = "1.4.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index bdb9b19..75b3c3f 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.3.2" +version = "1.4.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From 7e9d12d94f6bb9359282988bbb2f8fbeb552369b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 01:43:50 +0000 Subject: [PATCH 063/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 32808d2..56f5a81 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: ab495a165f0919b37cbf9efbd0f0e6ef +config_hash: 41c337f5cda03b13880617490f82bad0 From 2af0a63a24156c60814cde4ffe602f1f905b8bf5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 01:44:05 +0000 Subject: [PATCH 064/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 56f5a81..32808d2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: 41c337f5cda03b13880617490f82bad0 +config_hash: ab495a165f0919b37cbf9efbd0f0e6ef From 7e6120a505da7f47df2b60140efb795be7ee76c3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 01:44:27 +0000 Subject: [PATCH 065/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 32808d2..56f5a81 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f -config_hash: ab495a165f0919b37cbf9efbd0f0e6ef +config_hash: 41c337f5cda03b13880617490f82bad0 From 59cbe7a6554115ff75f2ac62300870d6f67af346 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 05:23:02 +0000 Subject: [PATCH 066/116] chore(internal): remove mock server code --- scripts/mock | 41 ----------------------------------------- scripts/test | 46 ---------------------------------------------- 2 files changed, 87 deletions(-) delete mode 100755 scripts/mock diff --git a/scripts/mock b/scripts/mock deleted file mode 100755 index 0b28f6e..0000000 --- a/scripts/mock +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [[ -n "$1" && "$1" != '--'* ]]; then - URL="$1" - shift -else - URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" -fi - -# Check if the URL is empty -if [ -z "$URL" ]; then - echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" - exit 1 -fi - -echo "==> Starting mock server with URL ${URL}" - -# Run prism mock on the given spec -if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - - # Wait for server to come online - echo -n "Waiting for server" - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do - echo -n "." - sleep 0.1 - done - - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - - echo -else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" -fi diff --git a/scripts/test b/scripts/test index b56970b..fe50ebb 100755 --- a/scripts/test +++ b/scripts/test @@ -4,53 +4,7 @@ set -e cd "$(dirname "$0")/.." -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 -} - -kill_server_on_port() { - pids=$(lsof -t -i tcp:"$1" || echo "") - if [ "$pids" != "" ]; then - kill "$pids" - echo "Stopped $pids." - fi -} - -function is_overriding_api_base_url() { - [ -n "$TEST_API_BASE_URL" ] -} - -if ! is_overriding_api_base_url && ! prism_is_running ; then - # When we exit this script, make sure to kill the background mock server process - trap 'kill_server_on_port 4010' EXIT - - # Start the dev server - ./scripts/mock --daemon -fi - -if is_overriding_api_base_url ; then - echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" - echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" - echo -e "running against your OpenAPI spec." - echo - echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" - echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" - echo - - exit 1 -else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" - echo -fi export DEFER_PYDANTIC_BUILD=false From 016aa9232f351a4d4ec805df251e0ba315b37f88 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 05:23:59 +0000 Subject: [PATCH 067/116] chore: update mock server docs --- CONTRIBUTING.md | 7 --- tests/api_resources/cdsl/test_fetch.py | 32 ++++++------- tests/api_resources/test_access_token.py | 16 +++---- tests/api_resources/test_cams_kfintech.py | 16 +++---- tests/api_resources/test_cdsl.py | 16 +++---- tests/api_resources/test_contract_note.py | 16 +++---- tests/api_resources/test_credits.py | 12 ++--- tests/api_resources/test_inbox.py | 56 +++++++++++------------ tests/api_resources/test_kfintech.py | 16 +++---- tests/api_resources/test_logs.py | 32 ++++++------- tests/api_resources/test_nsdl.py | 16 +++---- tests/api_resources/test_smart.py | 16 +++---- tests/api_resources/test_verify_token.py | 12 ++--- 13 files changed, 128 insertions(+), 135 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5274c21..718d320 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,13 +85,6 @@ $ pip install ./path-to-wheel-file.whl ## Running tests -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. - -```sh -# you will need npm installed -$ npx prism mock path/to/your/openapi.yml -``` - ```sh $ ./scripts/test ``` diff --git a/tests/api_resources/cdsl/test_fetch.py b/tests/api_resources/cdsl/test_fetch.py index a4b92cc..c97c019 100644 --- a/tests/api_resources/cdsl/test_fetch.py +++ b/tests/api_resources/cdsl/test_fetch.py @@ -20,7 +20,7 @@ class TestFetch: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_request_otp(self, client: CasParser) -> None: fetch = client.cdsl.fetch.request_otp( @@ -30,7 +30,7 @@ def test_method_request_otp(self, client: CasParser) -> None: ) assert_matches_type(FetchRequestOtpResponse, fetch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_request_otp(self, client: CasParser) -> None: response = client.cdsl.fetch.with_raw_response.request_otp( @@ -44,7 +44,7 @@ def test_raw_response_request_otp(self, client: CasParser) -> None: fetch = response.parse() assert_matches_type(FetchRequestOtpResponse, fetch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_request_otp(self, client: CasParser) -> None: with client.cdsl.fetch.with_streaming_response.request_otp( @@ -60,7 +60,7 @@ def test_streaming_response_request_otp(self, client: CasParser) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_verify_otp(self, client: CasParser) -> None: fetch = client.cdsl.fetch.verify_otp( @@ -69,7 +69,7 @@ def test_method_verify_otp(self, client: CasParser) -> None: ) assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_verify_otp_with_all_params(self, client: CasParser) -> None: fetch = client.cdsl.fetch.verify_otp( @@ -79,7 +79,7 @@ def test_method_verify_otp_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_verify_otp(self, client: CasParser) -> None: response = client.cdsl.fetch.with_raw_response.verify_otp( @@ -92,7 +92,7 @@ def test_raw_response_verify_otp(self, client: CasParser) -> None: fetch = response.parse() assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_verify_otp(self, client: CasParser) -> None: with client.cdsl.fetch.with_streaming_response.verify_otp( @@ -107,7 +107,7 @@ def test_streaming_response_verify_otp(self, client: CasParser) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_verify_otp(self, client: CasParser) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): @@ -122,7 +122,7 @@ class TestAsyncFetch: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_request_otp(self, async_client: AsyncCasParser) -> None: fetch = await async_client.cdsl.fetch.request_otp( @@ -132,7 +132,7 @@ async def test_method_request_otp(self, async_client: AsyncCasParser) -> None: ) assert_matches_type(FetchRequestOtpResponse, fetch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_request_otp(self, async_client: AsyncCasParser) -> None: response = await async_client.cdsl.fetch.with_raw_response.request_otp( @@ -146,7 +146,7 @@ async def test_raw_response_request_otp(self, async_client: AsyncCasParser) -> N fetch = await response.parse() assert_matches_type(FetchRequestOtpResponse, fetch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_request_otp(self, async_client: AsyncCasParser) -> None: async with async_client.cdsl.fetch.with_streaming_response.request_otp( @@ -162,7 +162,7 @@ async def test_streaming_response_request_otp(self, async_client: AsyncCasParser assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_verify_otp(self, async_client: AsyncCasParser) -> None: fetch = await async_client.cdsl.fetch.verify_otp( @@ -171,7 +171,7 @@ async def test_method_verify_otp(self, async_client: AsyncCasParser) -> None: ) assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_verify_otp_with_all_params(self, async_client: AsyncCasParser) -> None: fetch = await async_client.cdsl.fetch.verify_otp( @@ -181,7 +181,7 @@ async def test_method_verify_otp_with_all_params(self, async_client: AsyncCasPar ) assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_verify_otp(self, async_client: AsyncCasParser) -> None: response = await async_client.cdsl.fetch.with_raw_response.verify_otp( @@ -194,7 +194,7 @@ async def test_raw_response_verify_otp(self, async_client: AsyncCasParser) -> No fetch = await response.parse() assert_matches_type(FetchVerifyOtpResponse, fetch, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_verify_otp(self, async_client: AsyncCasParser) -> None: async with async_client.cdsl.fetch.with_streaming_response.verify_otp( @@ -209,7 +209,7 @@ async def test_streaming_response_verify_otp(self, async_client: AsyncCasParser) assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_verify_otp(self, async_client: AsyncCasParser) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): diff --git a/tests/api_resources/test_access_token.py b/tests/api_resources/test_access_token.py index 3edd508..32d63c9 100644 --- a/tests/api_resources/test_access_token.py +++ b/tests/api_resources/test_access_token.py @@ -17,13 +17,13 @@ class TestAccessToken: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: CasParser) -> None: access_token = client.access_token.create() assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: CasParser) -> None: access_token = client.access_token.create( @@ -31,7 +31,7 @@ def test_method_create_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: CasParser) -> None: response = client.access_token.with_raw_response.create() @@ -41,7 +41,7 @@ def test_raw_response_create(self, client: CasParser) -> None: access_token = response.parse() assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: CasParser) -> None: with client.access_token.with_streaming_response.create() as response: @@ -59,13 +59,13 @@ class TestAsyncAccessToken: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncCasParser) -> None: access_token = await async_client.access_token.create() assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncCasParser) -> None: access_token = await async_client.access_token.create( @@ -73,7 +73,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncCasParser) ) assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: response = await async_client.access_token.with_raw_response.create() @@ -83,7 +83,7 @@ async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: access_token = await response.parse() assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncCasParser) -> None: async with async_client.access_token.with_streaming_response.create() as response: diff --git a/tests/api_resources/test_cams_kfintech.py b/tests/api_resources/test_cams_kfintech.py index b9d8d0e..362fa42 100644 --- a/tests/api_resources/test_cams_kfintech.py +++ b/tests/api_resources/test_cams_kfintech.py @@ -17,13 +17,13 @@ class TestCamsKfintech: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_parse(self, client: CasParser) -> None: cams_kfintech = client.cams_kfintech.parse() assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_parse_with_all_params(self, client: CasParser) -> None: cams_kfintech = client.cams_kfintech.parse( @@ -33,7 +33,7 @@ def test_method_parse_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_parse(self, client: CasParser) -> None: response = client.cams_kfintech.with_raw_response.parse() @@ -43,7 +43,7 @@ def test_raw_response_parse(self, client: CasParser) -> None: cams_kfintech = response.parse() assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_parse(self, client: CasParser) -> None: with client.cams_kfintech.with_streaming_response.parse() as response: @@ -61,13 +61,13 @@ class TestAsyncCamsKfintech: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_parse(self, async_client: AsyncCasParser) -> None: cams_kfintech = await async_client.cams_kfintech.parse() assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_parse_with_all_params(self, async_client: AsyncCasParser) -> None: cams_kfintech = await async_client.cams_kfintech.parse( @@ -77,7 +77,7 @@ async def test_method_parse_with_all_params(self, async_client: AsyncCasParser) ) assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_parse(self, async_client: AsyncCasParser) -> None: response = await async_client.cams_kfintech.with_raw_response.parse() @@ -87,7 +87,7 @@ async def test_raw_response_parse(self, async_client: AsyncCasParser) -> None: cams_kfintech = await response.parse() assert_matches_type(UnifiedResponse, cams_kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_parse(self, async_client: AsyncCasParser) -> None: async with async_client.cams_kfintech.with_streaming_response.parse() as response: diff --git a/tests/api_resources/test_cdsl.py b/tests/api_resources/test_cdsl.py index a047f79..781e725 100644 --- a/tests/api_resources/test_cdsl.py +++ b/tests/api_resources/test_cdsl.py @@ -17,13 +17,13 @@ class TestCdsl: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_parse_pdf(self, client: CasParser) -> None: cdsl = client.cdsl.parse_pdf() assert_matches_type(UnifiedResponse, cdsl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_parse_pdf_with_all_params(self, client: CasParser) -> None: cdsl = client.cdsl.parse_pdf( @@ -33,7 +33,7 @@ def test_method_parse_pdf_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(UnifiedResponse, cdsl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_parse_pdf(self, client: CasParser) -> None: response = client.cdsl.with_raw_response.parse_pdf() @@ -43,7 +43,7 @@ def test_raw_response_parse_pdf(self, client: CasParser) -> None: cdsl = response.parse() assert_matches_type(UnifiedResponse, cdsl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_parse_pdf(self, client: CasParser) -> None: with client.cdsl.with_streaming_response.parse_pdf() as response: @@ -61,13 +61,13 @@ class TestAsyncCdsl: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_parse_pdf(self, async_client: AsyncCasParser) -> None: cdsl = await async_client.cdsl.parse_pdf() assert_matches_type(UnifiedResponse, cdsl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_parse_pdf_with_all_params(self, async_client: AsyncCasParser) -> None: cdsl = await async_client.cdsl.parse_pdf( @@ -77,7 +77,7 @@ async def test_method_parse_pdf_with_all_params(self, async_client: AsyncCasPars ) assert_matches_type(UnifiedResponse, cdsl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_parse_pdf(self, async_client: AsyncCasParser) -> None: response = await async_client.cdsl.with_raw_response.parse_pdf() @@ -87,7 +87,7 @@ async def test_raw_response_parse_pdf(self, async_client: AsyncCasParser) -> Non cdsl = await response.parse() assert_matches_type(UnifiedResponse, cdsl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_parse_pdf(self, async_client: AsyncCasParser) -> None: async with async_client.cdsl.with_streaming_response.parse_pdf() as response: diff --git a/tests/api_resources/test_contract_note.py b/tests/api_resources/test_contract_note.py index 5e39156..e19d592 100644 --- a/tests/api_resources/test_contract_note.py +++ b/tests/api_resources/test_contract_note.py @@ -17,13 +17,13 @@ class TestContractNote: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_parse(self, client: CasParser) -> None: contract_note = client.contract_note.parse() assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_parse_with_all_params(self, client: CasParser) -> None: contract_note = client.contract_note.parse( @@ -34,7 +34,7 @@ def test_method_parse_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_parse(self, client: CasParser) -> None: response = client.contract_note.with_raw_response.parse() @@ -44,7 +44,7 @@ def test_raw_response_parse(self, client: CasParser) -> None: contract_note = response.parse() assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_parse(self, client: CasParser) -> None: with client.contract_note.with_streaming_response.parse() as response: @@ -62,13 +62,13 @@ class TestAsyncContractNote: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_parse(self, async_client: AsyncCasParser) -> None: contract_note = await async_client.contract_note.parse() assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_parse_with_all_params(self, async_client: AsyncCasParser) -> None: contract_note = await async_client.contract_note.parse( @@ -79,7 +79,7 @@ async def test_method_parse_with_all_params(self, async_client: AsyncCasParser) ) assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_parse(self, async_client: AsyncCasParser) -> None: response = await async_client.contract_note.with_raw_response.parse() @@ -89,7 +89,7 @@ async def test_raw_response_parse(self, async_client: AsyncCasParser) -> None: contract_note = await response.parse() assert_matches_type(ContractNoteParseResponse, contract_note, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_parse(self, async_client: AsyncCasParser) -> None: async with async_client.contract_note.with_streaming_response.parse() as response: diff --git a/tests/api_resources/test_credits.py b/tests/api_resources/test_credits.py index e761b62..8538889 100644 --- a/tests/api_resources/test_credits.py +++ b/tests/api_resources/test_credits.py @@ -17,13 +17,13 @@ class TestCredits: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_check(self, client: CasParser) -> None: credit = client.credits.check() assert_matches_type(CreditCheckResponse, credit, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_check(self, client: CasParser) -> None: response = client.credits.with_raw_response.check() @@ -33,7 +33,7 @@ def test_raw_response_check(self, client: CasParser) -> None: credit = response.parse() assert_matches_type(CreditCheckResponse, credit, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_check(self, client: CasParser) -> None: with client.credits.with_streaming_response.check() as response: @@ -51,13 +51,13 @@ class TestAsyncCredits: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_check(self, async_client: AsyncCasParser) -> None: credit = await async_client.credits.check() assert_matches_type(CreditCheckResponse, credit, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_check(self, async_client: AsyncCasParser) -> None: response = await async_client.credits.with_raw_response.check() @@ -67,7 +67,7 @@ async def test_raw_response_check(self, async_client: AsyncCasParser) -> None: credit = await response.parse() assert_matches_type(CreditCheckResponse, credit, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_check(self, async_client: AsyncCasParser) -> None: async with async_client.credits.with_streaming_response.check() as response: diff --git a/tests/api_resources/test_inbox.py b/tests/api_resources/test_inbox.py index d41ebc8..3924127 100644 --- a/tests/api_resources/test_inbox.py +++ b/tests/api_resources/test_inbox.py @@ -23,7 +23,7 @@ class TestInbox: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_check_connection_status(self, client: CasParser) -> None: inbox = client.inbox.check_connection_status( @@ -31,7 +31,7 @@ def test_method_check_connection_status(self, client: CasParser) -> None: ) assert_matches_type(InboxCheckConnectionStatusResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_check_connection_status(self, client: CasParser) -> None: response = client.inbox.with_raw_response.check_connection_status( @@ -43,7 +43,7 @@ def test_raw_response_check_connection_status(self, client: CasParser) -> None: inbox = response.parse() assert_matches_type(InboxCheckConnectionStatusResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_check_connection_status(self, client: CasParser) -> None: with client.inbox.with_streaming_response.check_connection_status( @@ -57,7 +57,7 @@ def test_streaming_response_check_connection_status(self, client: CasParser) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_connect_email(self, client: CasParser) -> None: inbox = client.inbox.connect_email( @@ -65,7 +65,7 @@ def test_method_connect_email(self, client: CasParser) -> None: ) assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_connect_email_with_all_params(self, client: CasParser) -> None: inbox = client.inbox.connect_email( @@ -74,7 +74,7 @@ def test_method_connect_email_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_connect_email(self, client: CasParser) -> None: response = client.inbox.with_raw_response.connect_email( @@ -86,7 +86,7 @@ def test_raw_response_connect_email(self, client: CasParser) -> None: inbox = response.parse() assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_connect_email(self, client: CasParser) -> None: with client.inbox.with_streaming_response.connect_email( @@ -100,7 +100,7 @@ def test_streaming_response_connect_email(self, client: CasParser) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_disconnect_email(self, client: CasParser) -> None: inbox = client.inbox.disconnect_email( @@ -108,7 +108,7 @@ def test_method_disconnect_email(self, client: CasParser) -> None: ) assert_matches_type(InboxDisconnectEmailResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_disconnect_email(self, client: CasParser) -> None: response = client.inbox.with_raw_response.disconnect_email( @@ -120,7 +120,7 @@ def test_raw_response_disconnect_email(self, client: CasParser) -> None: inbox = response.parse() assert_matches_type(InboxDisconnectEmailResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_disconnect_email(self, client: CasParser) -> None: with client.inbox.with_streaming_response.disconnect_email( @@ -134,7 +134,7 @@ def test_streaming_response_disconnect_email(self, client: CasParser) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_cas_files(self, client: CasParser) -> None: inbox = client.inbox.list_cas_files( @@ -142,7 +142,7 @@ def test_method_list_cas_files(self, client: CasParser) -> None: ) assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list_cas_files_with_all_params(self, client: CasParser) -> None: inbox = client.inbox.list_cas_files( @@ -153,7 +153,7 @@ def test_method_list_cas_files_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_list_cas_files(self, client: CasParser) -> None: response = client.inbox.with_raw_response.list_cas_files( @@ -165,7 +165,7 @@ def test_raw_response_list_cas_files(self, client: CasParser) -> None: inbox = response.parse() assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_list_cas_files(self, client: CasParser) -> None: with client.inbox.with_streaming_response.list_cas_files( @@ -185,7 +185,7 @@ class TestAsyncInbox: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_check_connection_status(self, async_client: AsyncCasParser) -> None: inbox = await async_client.inbox.check_connection_status( @@ -193,7 +193,7 @@ async def test_method_check_connection_status(self, async_client: AsyncCasParser ) assert_matches_type(InboxCheckConnectionStatusResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_check_connection_status(self, async_client: AsyncCasParser) -> None: response = await async_client.inbox.with_raw_response.check_connection_status( @@ -205,7 +205,7 @@ async def test_raw_response_check_connection_status(self, async_client: AsyncCas inbox = await response.parse() assert_matches_type(InboxCheckConnectionStatusResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_check_connection_status(self, async_client: AsyncCasParser) -> None: async with async_client.inbox.with_streaming_response.check_connection_status( @@ -219,7 +219,7 @@ async def test_streaming_response_check_connection_status(self, async_client: As assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_connect_email(self, async_client: AsyncCasParser) -> None: inbox = await async_client.inbox.connect_email( @@ -227,7 +227,7 @@ async def test_method_connect_email(self, async_client: AsyncCasParser) -> None: ) assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_connect_email_with_all_params(self, async_client: AsyncCasParser) -> None: inbox = await async_client.inbox.connect_email( @@ -236,7 +236,7 @@ async def test_method_connect_email_with_all_params(self, async_client: AsyncCas ) assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_connect_email(self, async_client: AsyncCasParser) -> None: response = await async_client.inbox.with_raw_response.connect_email( @@ -248,7 +248,7 @@ async def test_raw_response_connect_email(self, async_client: AsyncCasParser) -> inbox = await response.parse() assert_matches_type(InboxConnectEmailResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_connect_email(self, async_client: AsyncCasParser) -> None: async with async_client.inbox.with_streaming_response.connect_email( @@ -262,7 +262,7 @@ async def test_streaming_response_connect_email(self, async_client: AsyncCasPars assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_disconnect_email(self, async_client: AsyncCasParser) -> None: inbox = await async_client.inbox.disconnect_email( @@ -270,7 +270,7 @@ async def test_method_disconnect_email(self, async_client: AsyncCasParser) -> No ) assert_matches_type(InboxDisconnectEmailResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_disconnect_email(self, async_client: AsyncCasParser) -> None: response = await async_client.inbox.with_raw_response.disconnect_email( @@ -282,7 +282,7 @@ async def test_raw_response_disconnect_email(self, async_client: AsyncCasParser) inbox = await response.parse() assert_matches_type(InboxDisconnectEmailResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_disconnect_email(self, async_client: AsyncCasParser) -> None: async with async_client.inbox.with_streaming_response.disconnect_email( @@ -296,7 +296,7 @@ async def test_streaming_response_disconnect_email(self, async_client: AsyncCasP assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_cas_files(self, async_client: AsyncCasParser) -> None: inbox = await async_client.inbox.list_cas_files( @@ -304,7 +304,7 @@ async def test_method_list_cas_files(self, async_client: AsyncCasParser) -> None ) assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list_cas_files_with_all_params(self, async_client: AsyncCasParser) -> None: inbox = await async_client.inbox.list_cas_files( @@ -315,7 +315,7 @@ async def test_method_list_cas_files_with_all_params(self, async_client: AsyncCa ) assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_list_cas_files(self, async_client: AsyncCasParser) -> None: response = await async_client.inbox.with_raw_response.list_cas_files( @@ -327,7 +327,7 @@ async def test_raw_response_list_cas_files(self, async_client: AsyncCasParser) - inbox = await response.parse() assert_matches_type(InboxListCasFilesResponse, inbox, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_list_cas_files(self, async_client: AsyncCasParser) -> None: async with async_client.inbox.with_streaming_response.list_cas_files( diff --git a/tests/api_resources/test_kfintech.py b/tests/api_resources/test_kfintech.py index fee4673..1ad3779 100644 --- a/tests/api_resources/test_kfintech.py +++ b/tests/api_resources/test_kfintech.py @@ -17,7 +17,7 @@ class TestKfintech: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_generate_cas(self, client: CasParser) -> None: kfintech = client.kfintech.generate_cas( @@ -28,7 +28,7 @@ def test_method_generate_cas(self, client: CasParser) -> None: ) assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_generate_cas_with_all_params(self, client: CasParser) -> None: kfintech = client.kfintech.generate_cas( @@ -40,7 +40,7 @@ def test_method_generate_cas_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_generate_cas(self, client: CasParser) -> None: response = client.kfintech.with_raw_response.generate_cas( @@ -55,7 +55,7 @@ def test_raw_response_generate_cas(self, client: CasParser) -> None: kfintech = response.parse() assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_generate_cas(self, client: CasParser) -> None: with client.kfintech.with_streaming_response.generate_cas( @@ -78,7 +78,7 @@ class TestAsyncKfintech: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_generate_cas(self, async_client: AsyncCasParser) -> None: kfintech = await async_client.kfintech.generate_cas( @@ -89,7 +89,7 @@ async def test_method_generate_cas(self, async_client: AsyncCasParser) -> None: ) assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_generate_cas_with_all_params(self, async_client: AsyncCasParser) -> None: kfintech = await async_client.kfintech.generate_cas( @@ -101,7 +101,7 @@ async def test_method_generate_cas_with_all_params(self, async_client: AsyncCasP ) assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_generate_cas(self, async_client: AsyncCasParser) -> None: response = await async_client.kfintech.with_raw_response.generate_cas( @@ -116,7 +116,7 @@ async def test_raw_response_generate_cas(self, async_client: AsyncCasParser) -> kfintech = await response.parse() assert_matches_type(KfintechGenerateCasResponse, kfintech, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_generate_cas(self, async_client: AsyncCasParser) -> None: async with async_client.kfintech.with_streaming_response.generate_cas( diff --git a/tests/api_resources/test_logs.py b/tests/api_resources/test_logs.py index 226ca42..43908ef 100644 --- a/tests/api_resources/test_logs.py +++ b/tests/api_resources/test_logs.py @@ -18,13 +18,13 @@ class TestLogs: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: CasParser) -> None: log = client.logs.create() assert_matches_type(LogCreateResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: CasParser) -> None: log = client.logs.create( @@ -34,7 +34,7 @@ def test_method_create_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(LogCreateResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: CasParser) -> None: response = client.logs.with_raw_response.create() @@ -44,7 +44,7 @@ def test_raw_response_create(self, client: CasParser) -> None: log = response.parse() assert_matches_type(LogCreateResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: CasParser) -> None: with client.logs.with_streaming_response.create() as response: @@ -56,13 +56,13 @@ def test_streaming_response_create(self, client: CasParser) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_get_summary(self, client: CasParser) -> None: log = client.logs.get_summary() assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_get_summary_with_all_params(self, client: CasParser) -> None: log = client.logs.get_summary( @@ -71,7 +71,7 @@ def test_method_get_summary_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_get_summary(self, client: CasParser) -> None: response = client.logs.with_raw_response.get_summary() @@ -81,7 +81,7 @@ def test_raw_response_get_summary(self, client: CasParser) -> None: log = response.parse() assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_get_summary(self, client: CasParser) -> None: with client.logs.with_streaming_response.get_summary() as response: @@ -99,13 +99,13 @@ class TestAsyncLogs: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncCasParser) -> None: log = await async_client.logs.create() assert_matches_type(LogCreateResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncCasParser) -> None: log = await async_client.logs.create( @@ -115,7 +115,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncCasParser) ) assert_matches_type(LogCreateResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: response = await async_client.logs.with_raw_response.create() @@ -125,7 +125,7 @@ async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: log = await response.parse() assert_matches_type(LogCreateResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncCasParser) -> None: async with async_client.logs.with_streaming_response.create() as response: @@ -137,13 +137,13 @@ async def test_streaming_response_create(self, async_client: AsyncCasParser) -> assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_get_summary(self, async_client: AsyncCasParser) -> None: log = await async_client.logs.get_summary() assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_get_summary_with_all_params(self, async_client: AsyncCasParser) -> None: log = await async_client.logs.get_summary( @@ -152,7 +152,7 @@ async def test_method_get_summary_with_all_params(self, async_client: AsyncCasPa ) assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_get_summary(self, async_client: AsyncCasParser) -> None: response = await async_client.logs.with_raw_response.get_summary() @@ -162,7 +162,7 @@ async def test_raw_response_get_summary(self, async_client: AsyncCasParser) -> N log = await response.parse() assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_get_summary(self, async_client: AsyncCasParser) -> None: async with async_client.logs.with_streaming_response.get_summary() as response: diff --git a/tests/api_resources/test_nsdl.py b/tests/api_resources/test_nsdl.py index 6e30980..7a599fb 100644 --- a/tests/api_resources/test_nsdl.py +++ b/tests/api_resources/test_nsdl.py @@ -17,13 +17,13 @@ class TestNsdl: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_parse(self, client: CasParser) -> None: nsdl = client.nsdl.parse() assert_matches_type(UnifiedResponse, nsdl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_parse_with_all_params(self, client: CasParser) -> None: nsdl = client.nsdl.parse( @@ -33,7 +33,7 @@ def test_method_parse_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(UnifiedResponse, nsdl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_parse(self, client: CasParser) -> None: response = client.nsdl.with_raw_response.parse() @@ -43,7 +43,7 @@ def test_raw_response_parse(self, client: CasParser) -> None: nsdl = response.parse() assert_matches_type(UnifiedResponse, nsdl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_parse(self, client: CasParser) -> None: with client.nsdl.with_streaming_response.parse() as response: @@ -61,13 +61,13 @@ class TestAsyncNsdl: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_parse(self, async_client: AsyncCasParser) -> None: nsdl = await async_client.nsdl.parse() assert_matches_type(UnifiedResponse, nsdl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_parse_with_all_params(self, async_client: AsyncCasParser) -> None: nsdl = await async_client.nsdl.parse( @@ -77,7 +77,7 @@ async def test_method_parse_with_all_params(self, async_client: AsyncCasParser) ) assert_matches_type(UnifiedResponse, nsdl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_parse(self, async_client: AsyncCasParser) -> None: response = await async_client.nsdl.with_raw_response.parse() @@ -87,7 +87,7 @@ async def test_raw_response_parse(self, async_client: AsyncCasParser) -> None: nsdl = await response.parse() assert_matches_type(UnifiedResponse, nsdl, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_parse(self, async_client: AsyncCasParser) -> None: async with async_client.nsdl.with_streaming_response.parse() as response: diff --git a/tests/api_resources/test_smart.py b/tests/api_resources/test_smart.py index 152457b..074c248 100644 --- a/tests/api_resources/test_smart.py +++ b/tests/api_resources/test_smart.py @@ -17,13 +17,13 @@ class TestSmart: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_parse_cas_pdf(self, client: CasParser) -> None: smart = client.smart.parse_cas_pdf() assert_matches_type(UnifiedResponse, smart, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_parse_cas_pdf_with_all_params(self, client: CasParser) -> None: smart = client.smart.parse_cas_pdf( @@ -33,7 +33,7 @@ def test_method_parse_cas_pdf_with_all_params(self, client: CasParser) -> None: ) assert_matches_type(UnifiedResponse, smart, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_parse_cas_pdf(self, client: CasParser) -> None: response = client.smart.with_raw_response.parse_cas_pdf() @@ -43,7 +43,7 @@ def test_raw_response_parse_cas_pdf(self, client: CasParser) -> None: smart = response.parse() assert_matches_type(UnifiedResponse, smart, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_parse_cas_pdf(self, client: CasParser) -> None: with client.smart.with_streaming_response.parse_cas_pdf() as response: @@ -61,13 +61,13 @@ class TestAsyncSmart: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_parse_cas_pdf(self, async_client: AsyncCasParser) -> None: smart = await async_client.smart.parse_cas_pdf() assert_matches_type(UnifiedResponse, smart, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_parse_cas_pdf_with_all_params(self, async_client: AsyncCasParser) -> None: smart = await async_client.smart.parse_cas_pdf( @@ -77,7 +77,7 @@ async def test_method_parse_cas_pdf_with_all_params(self, async_client: AsyncCas ) assert_matches_type(UnifiedResponse, smart, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_parse_cas_pdf(self, async_client: AsyncCasParser) -> None: response = await async_client.smart.with_raw_response.parse_cas_pdf() @@ -87,7 +87,7 @@ async def test_raw_response_parse_cas_pdf(self, async_client: AsyncCasParser) -> smart = await response.parse() assert_matches_type(UnifiedResponse, smart, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_parse_cas_pdf(self, async_client: AsyncCasParser) -> None: async with async_client.smart.with_streaming_response.parse_cas_pdf() as response: diff --git a/tests/api_resources/test_verify_token.py b/tests/api_resources/test_verify_token.py index 2ff8a90..43e594d 100644 --- a/tests/api_resources/test_verify_token.py +++ b/tests/api_resources/test_verify_token.py @@ -17,13 +17,13 @@ class TestVerifyToken: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_verify(self, client: CasParser) -> None: verify_token = client.verify_token.verify() assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_verify(self, client: CasParser) -> None: response = client.verify_token.with_raw_response.verify() @@ -33,7 +33,7 @@ def test_raw_response_verify(self, client: CasParser) -> None: verify_token = response.parse() assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_verify(self, client: CasParser) -> None: with client.verify_token.with_streaming_response.verify() as response: @@ -51,13 +51,13 @@ class TestAsyncVerifyToken: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_verify(self, async_client: AsyncCasParser) -> None: verify_token = await async_client.verify_token.verify() assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_verify(self, async_client: AsyncCasParser) -> None: response = await async_client.verify_token.with_raw_response.verify() @@ -67,7 +67,7 @@ async def test_raw_response_verify(self, async_client: AsyncCasParser) -> None: verify_token = await response.parse() assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_verify(self, async_client: AsyncCasParser) -> None: async with async_client.verify_token.with_streaming_response.verify() as response: From 034cf63e7315efd80336a0f5ff176f646ec77853 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 17:18:38 +0000 Subject: [PATCH 068/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3e9af1b..7325798 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.4.0" + ".": "1.4.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 54fc544..e9c0aea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.4.0" +version = "1.4.1" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 682501f..eaa6152 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.4.0" # x-release-please-version +__version__ = "1.4.1" # x-release-please-version diff --git a/uv.lock b/uv.lock index 75b3c3f..4e4a105 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.4.0" +version = "1.4.1" source = { editable = "." } dependencies = [ { name = "anyio" }, From 3e02d1057ef222a06e62bb768751a90a27703b10 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:17:43 +0000 Subject: [PATCH 069/116] feat(api): api update --- .stats.yml | 6 +- README.md | 26 +- api.md | 49 --- src/cas_parser/_client.py | 162 +-------- src/cas_parser/resources/__init__.py | 56 ---- src/cas_parser/resources/access_token.py | 191 ----------- src/cas_parser/resources/credits.py | 155 --------- src/cas_parser/resources/logs.py | 307 ------------------ src/cas_parser/resources/verify_token.py | 143 -------- src/cas_parser/types/__init__.py | 8 - .../types/access_token_create_params.py | 12 - .../types/access_token_create_response.py | 18 - src/cas_parser/types/credit_check_response.py | 28 -- src/cas_parser/types/log_create_params.py | 22 -- src/cas_parser/types/log_create_response.py | 37 --- .../types/log_get_summary_params.py | 19 -- .../types/log_get_summary_response.py | 35 -- .../types/verify_token_verify_response.py | 18 - tests/api_resources/test_access_token.py | 96 ------ tests/api_resources/test_credits.py | 80 ----- tests/api_resources/test_logs.py | 175 ---------- tests/api_resources/test_verify_token.py | 80 ----- tests/test_client.py | 40 +-- 23 files changed, 37 insertions(+), 1726 deletions(-) delete mode 100644 src/cas_parser/resources/access_token.py delete mode 100644 src/cas_parser/resources/credits.py delete mode 100644 src/cas_parser/resources/logs.py delete mode 100644 src/cas_parser/resources/verify_token.py delete mode 100644 src/cas_parser/types/access_token_create_params.py delete mode 100644 src/cas_parser/types/access_token_create_response.py delete mode 100644 src/cas_parser/types/credit_check_response.py delete mode 100644 src/cas_parser/types/log_create_params.py delete mode 100644 src/cas_parser/types/log_create_response.py delete mode 100644 src/cas_parser/types/log_get_summary_params.py delete mode 100644 src/cas_parser/types/log_get_summary_response.py delete mode 100644 src/cas_parser/types/verify_token_verify_response.py delete mode 100644 tests/api_resources/test_access_token.py delete mode 100644 tests/api_resources/test_credits.py delete mode 100644 tests/api_resources/test_logs.py delete mode 100644 tests/api_resources/test_verify_token.py diff --git a/.stats.yml b/.stats.yml index 56f5a81..80a6900 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 17 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-56b0f699c5437d9e5326626d35dfc972c17d01f12cb416c7f4854c8ea6d0e95e.yml -openapi_spec_hash: 158f405c1880706266d83e6ff16b9d2f +configured_endpoints: 12 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-6a9d3b677dcfb856dc571865c34b3fe401e4d7f0d799edfc743acb9a55800bd0.yml +openapi_spec_hash: 037703a6c741e4310fda3f57c22fa51e config_hash: 41c337f5cda03b13880617490f82bad0 diff --git a/README.md b/README.md index f7b645a..04c0f3e 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ client = CasParser( environment="environment_1", ) -response = client.credits.check() -print(response.enabled_features) +unified_response = client.cams_kfintech.parse() +print(unified_response.demat_accounts) ``` While you can provide an `api_key` keyword argument, @@ -69,8 +69,8 @@ client = AsyncCasParser( async def main() -> None: - response = await client.credits.check() - print(response.enabled_features) + unified_response = await client.cams_kfintech.parse() + print(unified_response.demat_accounts) asyncio.run(main()) @@ -103,8 +103,8 @@ async def main() -> None: api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: - response = await client.credits.check() - print(response.enabled_features) + unified_response = await client.cams_kfintech.parse() + print(unified_response.demat_accounts) asyncio.run(main()) @@ -135,7 +135,7 @@ from cas_parser import CasParser client = CasParser() try: - client.credits.check() + client.cams_kfintech.parse() except cas_parser.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. @@ -178,7 +178,7 @@ client = CasParser( ) # Or, configure per-request: -client.with_options(max_retries=5).credits.check() +client.with_options(max_retries=5).cams_kfintech.parse() ``` ### Timeouts @@ -201,7 +201,7 @@ client = CasParser( ) # Override per-request: -client.with_options(timeout=5.0).credits.check() +client.with_options(timeout=5.0).cams_kfintech.parse() ``` On timeout, an `APITimeoutError` is thrown. @@ -242,11 +242,11 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from cas_parser import CasParser client = CasParser() -response = client.credits.with_raw_response.check() +response = client.cams_kfintech.with_raw_response.parse() print(response.headers.get('X-My-Header')) -credit = response.parse() # get the object that `credits.check()` would have returned -print(credit.enabled_features) +cams_kfintech = response.parse() # get the object that `cams_kfintech.parse()` would have returned +print(cams_kfintech.demat_accounts) ``` These methods return an [`APIResponse`](https://github.com/CASParser/cas-parser-python/tree/main/src/cas_parser/_response.py) object. @@ -260,7 +260,7 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.credits.with_streaming_response.check() as response: +with client.cams_kfintech.with_streaming_response.parse() as response: print(response.headers.get("X-My-Header")) for line in response.iter_lines(): diff --git a/api.md b/api.md index d92590b..8ba2d2c 100644 --- a/api.md +++ b/api.md @@ -1,52 +1,3 @@ -# Credits - -Types: - -```python -from cas_parser.types import CreditCheckResponse -``` - -Methods: - -- client.credits.check() -> CreditCheckResponse - -# Logs - -Types: - -```python -from cas_parser.types import LogCreateResponse, LogGetSummaryResponse -``` - -Methods: - -- client.logs.create(\*\*params) -> LogCreateResponse -- client.logs.get_summary(\*\*params) -> LogGetSummaryResponse - -# AccessToken - -Types: - -```python -from cas_parser.types import AccessTokenCreateResponse -``` - -Methods: - -- client.access_token.create(\*\*params) -> AccessTokenCreateResponse - -# VerifyToken - -Types: - -```python -from cas_parser.types import VerifyTokenVerifyResponse -``` - -Methods: - -- client.verify_token.verify() -> VerifyTokenVerifyResponse - # CamsKfintech Types: diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py index 02dc18a..8041bb8 100644 --- a/src/cas_parser/_client.py +++ b/src/cas_parser/_client.py @@ -31,28 +31,12 @@ ) if TYPE_CHECKING: - from .resources import ( - cdsl, - logs, - nsdl, - inbox, - smart, - credits, - kfintech, - access_token, - verify_token, - cams_kfintech, - contract_note, - ) - from .resources.logs import LogsResource, AsyncLogsResource + from .resources import cdsl, nsdl, inbox, smart, kfintech, cams_kfintech, contract_note from .resources.nsdl import NsdlResource, AsyncNsdlResource from .resources.inbox import InboxResource, AsyncInboxResource from .resources.smart import SmartResource, AsyncSmartResource - from .resources.credits import CreditsResource, AsyncCreditsResource from .resources.kfintech import KfintechResource, AsyncKfintechResource from .resources.cdsl.cdsl import CdslResource, AsyncCdslResource - from .resources.access_token import AccessTokenResource, AsyncAccessTokenResource - from .resources.verify_token import VerifyTokenResource, AsyncVerifyTokenResource from .resources.cams_kfintech import CamsKfintechResource, AsyncCamsKfintechResource from .resources.contract_note import ContractNoteResource, AsyncContractNoteResource @@ -154,30 +138,6 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - @cached_property - def credits(self) -> CreditsResource: - from .resources.credits import CreditsResource - - return CreditsResource(self) - - @cached_property - def logs(self) -> LogsResource: - from .resources.logs import LogsResource - - return LogsResource(self) - - @cached_property - def access_token(self) -> AccessTokenResource: - from .resources.access_token import AccessTokenResource - - return AccessTokenResource(self) - - @cached_property - def verify_token(self) -> VerifyTokenResource: - from .resources.verify_token import VerifyTokenResource - - return VerifyTokenResource(self) - @cached_property def cams_kfintech(self) -> CamsKfintechResource: from .resources.cams_kfintech import CamsKfintechResource @@ -414,30 +374,6 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - @cached_property - def credits(self) -> AsyncCreditsResource: - from .resources.credits import AsyncCreditsResource - - return AsyncCreditsResource(self) - - @cached_property - def logs(self) -> AsyncLogsResource: - from .resources.logs import AsyncLogsResource - - return AsyncLogsResource(self) - - @cached_property - def access_token(self) -> AsyncAccessTokenResource: - from .resources.access_token import AsyncAccessTokenResource - - return AsyncAccessTokenResource(self) - - @cached_property - def verify_token(self) -> AsyncVerifyTokenResource: - from .resources.verify_token import AsyncVerifyTokenResource - - return AsyncVerifyTokenResource(self) - @cached_property def cams_kfintech(self) -> AsyncCamsKfintechResource: from .resources.cams_kfintech import AsyncCamsKfintechResource @@ -601,30 +537,6 @@ class CasParserWithRawResponse: def __init__(self, client: CasParser) -> None: self._client = client - @cached_property - def credits(self) -> credits.CreditsResourceWithRawResponse: - from .resources.credits import CreditsResourceWithRawResponse - - return CreditsResourceWithRawResponse(self._client.credits) - - @cached_property - def logs(self) -> logs.LogsResourceWithRawResponse: - from .resources.logs import LogsResourceWithRawResponse - - return LogsResourceWithRawResponse(self._client.logs) - - @cached_property - def access_token(self) -> access_token.AccessTokenResourceWithRawResponse: - from .resources.access_token import AccessTokenResourceWithRawResponse - - return AccessTokenResourceWithRawResponse(self._client.access_token) - - @cached_property - def verify_token(self) -> verify_token.VerifyTokenResourceWithRawResponse: - from .resources.verify_token import VerifyTokenResourceWithRawResponse - - return VerifyTokenResourceWithRawResponse(self._client.verify_token) - @cached_property def cams_kfintech(self) -> cams_kfintech.CamsKfintechResourceWithRawResponse: from .resources.cams_kfintech import CamsKfintechResourceWithRawResponse @@ -674,30 +586,6 @@ class AsyncCasParserWithRawResponse: def __init__(self, client: AsyncCasParser) -> None: self._client = client - @cached_property - def credits(self) -> credits.AsyncCreditsResourceWithRawResponse: - from .resources.credits import AsyncCreditsResourceWithRawResponse - - return AsyncCreditsResourceWithRawResponse(self._client.credits) - - @cached_property - def logs(self) -> logs.AsyncLogsResourceWithRawResponse: - from .resources.logs import AsyncLogsResourceWithRawResponse - - return AsyncLogsResourceWithRawResponse(self._client.logs) - - @cached_property - def access_token(self) -> access_token.AsyncAccessTokenResourceWithRawResponse: - from .resources.access_token import AsyncAccessTokenResourceWithRawResponse - - return AsyncAccessTokenResourceWithRawResponse(self._client.access_token) - - @cached_property - def verify_token(self) -> verify_token.AsyncVerifyTokenResourceWithRawResponse: - from .resources.verify_token import AsyncVerifyTokenResourceWithRawResponse - - return AsyncVerifyTokenResourceWithRawResponse(self._client.verify_token) - @cached_property def cams_kfintech(self) -> cams_kfintech.AsyncCamsKfintechResourceWithRawResponse: from .resources.cams_kfintech import AsyncCamsKfintechResourceWithRawResponse @@ -747,30 +635,6 @@ class CasParserWithStreamedResponse: def __init__(self, client: CasParser) -> None: self._client = client - @cached_property - def credits(self) -> credits.CreditsResourceWithStreamingResponse: - from .resources.credits import CreditsResourceWithStreamingResponse - - return CreditsResourceWithStreamingResponse(self._client.credits) - - @cached_property - def logs(self) -> logs.LogsResourceWithStreamingResponse: - from .resources.logs import LogsResourceWithStreamingResponse - - return LogsResourceWithStreamingResponse(self._client.logs) - - @cached_property - def access_token(self) -> access_token.AccessTokenResourceWithStreamingResponse: - from .resources.access_token import AccessTokenResourceWithStreamingResponse - - return AccessTokenResourceWithStreamingResponse(self._client.access_token) - - @cached_property - def verify_token(self) -> verify_token.VerifyTokenResourceWithStreamingResponse: - from .resources.verify_token import VerifyTokenResourceWithStreamingResponse - - return VerifyTokenResourceWithStreamingResponse(self._client.verify_token) - @cached_property def cams_kfintech(self) -> cams_kfintech.CamsKfintechResourceWithStreamingResponse: from .resources.cams_kfintech import CamsKfintechResourceWithStreamingResponse @@ -820,30 +684,6 @@ class AsyncCasParserWithStreamedResponse: def __init__(self, client: AsyncCasParser) -> None: self._client = client - @cached_property - def credits(self) -> credits.AsyncCreditsResourceWithStreamingResponse: - from .resources.credits import AsyncCreditsResourceWithStreamingResponse - - return AsyncCreditsResourceWithStreamingResponse(self._client.credits) - - @cached_property - def logs(self) -> logs.AsyncLogsResourceWithStreamingResponse: - from .resources.logs import AsyncLogsResourceWithStreamingResponse - - return AsyncLogsResourceWithStreamingResponse(self._client.logs) - - @cached_property - def access_token(self) -> access_token.AsyncAccessTokenResourceWithStreamingResponse: - from .resources.access_token import AsyncAccessTokenResourceWithStreamingResponse - - return AsyncAccessTokenResourceWithStreamingResponse(self._client.access_token) - - @cached_property - def verify_token(self) -> verify_token.AsyncVerifyTokenResourceWithStreamingResponse: - from .resources.verify_token import AsyncVerifyTokenResourceWithStreamingResponse - - return AsyncVerifyTokenResourceWithStreamingResponse(self._client.verify_token) - @cached_property def cams_kfintech(self) -> cams_kfintech.AsyncCamsKfintechResourceWithStreamingResponse: from .resources.cams_kfintech import AsyncCamsKfintechResourceWithStreamingResponse diff --git a/src/cas_parser/resources/__init__.py b/src/cas_parser/resources/__init__.py index ac91596..4125404 100644 --- a/src/cas_parser/resources/__init__.py +++ b/src/cas_parser/resources/__init__.py @@ -8,14 +8,6 @@ CdslResourceWithStreamingResponse, AsyncCdslResourceWithStreamingResponse, ) -from .logs import ( - LogsResource, - AsyncLogsResource, - LogsResourceWithRawResponse, - AsyncLogsResourceWithRawResponse, - LogsResourceWithStreamingResponse, - AsyncLogsResourceWithStreamingResponse, -) from .nsdl import ( NsdlResource, AsyncNsdlResource, @@ -40,14 +32,6 @@ SmartResourceWithStreamingResponse, AsyncSmartResourceWithStreamingResponse, ) -from .credits import ( - CreditsResource, - AsyncCreditsResource, - CreditsResourceWithRawResponse, - AsyncCreditsResourceWithRawResponse, - CreditsResourceWithStreamingResponse, - AsyncCreditsResourceWithStreamingResponse, -) from .kfintech import ( KfintechResource, AsyncKfintechResource, @@ -56,22 +40,6 @@ KfintechResourceWithStreamingResponse, AsyncKfintechResourceWithStreamingResponse, ) -from .access_token import ( - AccessTokenResource, - AsyncAccessTokenResource, - AccessTokenResourceWithRawResponse, - AsyncAccessTokenResourceWithRawResponse, - AccessTokenResourceWithStreamingResponse, - AsyncAccessTokenResourceWithStreamingResponse, -) -from .verify_token import ( - VerifyTokenResource, - AsyncVerifyTokenResource, - VerifyTokenResourceWithRawResponse, - AsyncVerifyTokenResourceWithRawResponse, - VerifyTokenResourceWithStreamingResponse, - AsyncVerifyTokenResourceWithStreamingResponse, -) from .cams_kfintech import ( CamsKfintechResource, AsyncCamsKfintechResource, @@ -90,30 +58,6 @@ ) __all__ = [ - "CreditsResource", - "AsyncCreditsResource", - "CreditsResourceWithRawResponse", - "AsyncCreditsResourceWithRawResponse", - "CreditsResourceWithStreamingResponse", - "AsyncCreditsResourceWithStreamingResponse", - "LogsResource", - "AsyncLogsResource", - "LogsResourceWithRawResponse", - "AsyncLogsResourceWithRawResponse", - "LogsResourceWithStreamingResponse", - "AsyncLogsResourceWithStreamingResponse", - "AccessTokenResource", - "AsyncAccessTokenResource", - "AccessTokenResourceWithRawResponse", - "AsyncAccessTokenResourceWithRawResponse", - "AccessTokenResourceWithStreamingResponse", - "AsyncAccessTokenResourceWithStreamingResponse", - "VerifyTokenResource", - "AsyncVerifyTokenResource", - "VerifyTokenResourceWithRawResponse", - "AsyncVerifyTokenResourceWithRawResponse", - "VerifyTokenResourceWithStreamingResponse", - "AsyncVerifyTokenResourceWithStreamingResponse", "CamsKfintechResource", "AsyncCamsKfintechResource", "CamsKfintechResourceWithRawResponse", diff --git a/src/cas_parser/resources/access_token.py b/src/cas_parser/resources/access_token.py deleted file mode 100644 index aa92680..0000000 --- a/src/cas_parser/resources/access_token.py +++ /dev/null @@ -1,191 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from ..types import access_token_create_params -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.access_token_create_response import AccessTokenCreateResponse - -__all__ = ["AccessTokenResource", "AsyncAccessTokenResource"] - - -class AccessTokenResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> AccessTokenResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return AccessTokenResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AccessTokenResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return AccessTokenResourceWithStreamingResponse(self) - - def create( - self, - *, - expiry_minutes: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AccessTokenCreateResponse: - """ - Generate a short-lived access token from your API key. - - **Use this endpoint from your backend** to create tokens that can be safely - passed to frontend/SDK. - - Access tokens: - - - Are prefixed with `at_` for easy identification - - Valid for up to 60 minutes - - Can be used in place of API keys on all v4 endpoints - - Cannot be used to generate other access tokens - - Args: - expiry_minutes: Token validity in minutes (max 60) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/v1/access-token", - body=maybe_transform( - {"expiry_minutes": expiry_minutes}, access_token_create_params.AccessTokenCreateParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AccessTokenCreateResponse, - ) - - -class AsyncAccessTokenResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncAccessTokenResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return AsyncAccessTokenResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncAccessTokenResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return AsyncAccessTokenResourceWithStreamingResponse(self) - - async def create( - self, - *, - expiry_minutes: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AccessTokenCreateResponse: - """ - Generate a short-lived access token from your API key. - - **Use this endpoint from your backend** to create tokens that can be safely - passed to frontend/SDK. - - Access tokens: - - - Are prefixed with `at_` for easy identification - - Valid for up to 60 minutes - - Can be used in place of API keys on all v4 endpoints - - Cannot be used to generate other access tokens - - Args: - expiry_minutes: Token validity in minutes (max 60) - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/v1/access-token", - body=await async_maybe_transform( - {"expiry_minutes": expiry_minutes}, access_token_create_params.AccessTokenCreateParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AccessTokenCreateResponse, - ) - - -class AccessTokenResourceWithRawResponse: - def __init__(self, access_token: AccessTokenResource) -> None: - self._access_token = access_token - - self.create = to_raw_response_wrapper( - access_token.create, - ) - - -class AsyncAccessTokenResourceWithRawResponse: - def __init__(self, access_token: AsyncAccessTokenResource) -> None: - self._access_token = access_token - - self.create = async_to_raw_response_wrapper( - access_token.create, - ) - - -class AccessTokenResourceWithStreamingResponse: - def __init__(self, access_token: AccessTokenResource) -> None: - self._access_token = access_token - - self.create = to_streamed_response_wrapper( - access_token.create, - ) - - -class AsyncAccessTokenResourceWithStreamingResponse: - def __init__(self, access_token: AsyncAccessTokenResource) -> None: - self._access_token = access_token - - self.create = async_to_streamed_response_wrapper( - access_token.create, - ) diff --git a/src/cas_parser/resources/credits.py b/src/cas_parser/resources/credits.py deleted file mode 100644 index 4a86474..0000000 --- a/src/cas_parser/resources/credits.py +++ /dev/null @@ -1,155 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from .._types import Body, Query, Headers, NotGiven, not_given -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.credit_check_response import CreditCheckResponse - -__all__ = ["CreditsResource", "AsyncCreditsResource"] - - -class CreditsResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> CreditsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return CreditsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> CreditsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return CreditsResourceWithStreamingResponse(self) - - def check( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> CreditCheckResponse: - """ - Check your remaining API credits and usage for the current billing period. - - Returns: - - - Number of API calls used and remaining credits - - Credit limit and reset date - - List of enabled features for your plan - - Credits reset at the start of each billing period. - """ - return self._post( - "/credits", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=CreditCheckResponse, - ) - - -class AsyncCreditsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncCreditsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return AsyncCreditsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncCreditsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return AsyncCreditsResourceWithStreamingResponse(self) - - async def check( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> CreditCheckResponse: - """ - Check your remaining API credits and usage for the current billing period. - - Returns: - - - Number of API calls used and remaining credits - - Credit limit and reset date - - List of enabled features for your plan - - Credits reset at the start of each billing period. - """ - return await self._post( - "/credits", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=CreditCheckResponse, - ) - - -class CreditsResourceWithRawResponse: - def __init__(self, credits: CreditsResource) -> None: - self._credits = credits - - self.check = to_raw_response_wrapper( - credits.check, - ) - - -class AsyncCreditsResourceWithRawResponse: - def __init__(self, credits: AsyncCreditsResource) -> None: - self._credits = credits - - self.check = async_to_raw_response_wrapper( - credits.check, - ) - - -class CreditsResourceWithStreamingResponse: - def __init__(self, credits: CreditsResource) -> None: - self._credits = credits - - self.check = to_streamed_response_wrapper( - credits.check, - ) - - -class AsyncCreditsResourceWithStreamingResponse: - def __init__(self, credits: AsyncCreditsResource) -> None: - self._credits = credits - - self.check = async_to_streamed_response_wrapper( - credits.check, - ) diff --git a/src/cas_parser/resources/logs.py b/src/cas_parser/resources/logs.py deleted file mode 100644 index bd6ec9b..0000000 --- a/src/cas_parser/resources/logs.py +++ /dev/null @@ -1,307 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Union -from datetime import datetime - -import httpx - -from ..types import log_create_params, log_get_summary_params -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.log_create_response import LogCreateResponse -from ..types.log_get_summary_response import LogGetSummaryResponse - -__all__ = ["LogsResource", "AsyncLogsResource"] - - -class LogsResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> LogsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return LogsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> LogsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return LogsResourceWithStreamingResponse(self) - - def create( - self, - *, - end_time: Union[str, datetime] | Omit = omit, - limit: int | Omit = omit, - start_time: Union[str, datetime] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> LogCreateResponse: - """ - Retrieve detailed API usage logs for your account. - - Returns a list of API calls with timestamps, features used, status codes, and - credits consumed. Useful for monitoring usage patterns and debugging. - - Args: - end_time: End time filter (ISO 8601). Defaults to now. - - limit: Maximum number of logs to return - - start_time: Start time filter (ISO 8601). Defaults to 30 days ago. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/logs", - body=maybe_transform( - { - "end_time": end_time, - "limit": limit, - "start_time": start_time, - }, - log_create_params.LogCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=LogCreateResponse, - ) - - def get_summary( - self, - *, - end_time: Union[str, datetime] | Omit = omit, - start_time: Union[str, datetime] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> LogGetSummaryResponse: - """ - Get aggregated usage statistics grouped by feature. - - Useful for understanding which API features are being used most and tracking - usage trends. - - Args: - end_time: End time filter (ISO 8601). Defaults to now. - - start_time: Start time filter (ISO 8601). Defaults to start of current month. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/logs/summary", - body=maybe_transform( - { - "end_time": end_time, - "start_time": start_time, - }, - log_get_summary_params.LogGetSummaryParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=LogGetSummaryResponse, - ) - - -class AsyncLogsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return AsyncLogsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncLogsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return AsyncLogsResourceWithStreamingResponse(self) - - async def create( - self, - *, - end_time: Union[str, datetime] | Omit = omit, - limit: int | Omit = omit, - start_time: Union[str, datetime] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> LogCreateResponse: - """ - Retrieve detailed API usage logs for your account. - - Returns a list of API calls with timestamps, features used, status codes, and - credits consumed. Useful for monitoring usage patterns and debugging. - - Args: - end_time: End time filter (ISO 8601). Defaults to now. - - limit: Maximum number of logs to return - - start_time: Start time filter (ISO 8601). Defaults to 30 days ago. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/logs", - body=await async_maybe_transform( - { - "end_time": end_time, - "limit": limit, - "start_time": start_time, - }, - log_create_params.LogCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=LogCreateResponse, - ) - - async def get_summary( - self, - *, - end_time: Union[str, datetime] | Omit = omit, - start_time: Union[str, datetime] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> LogGetSummaryResponse: - """ - Get aggregated usage statistics grouped by feature. - - Useful for understanding which API features are being used most and tracking - usage trends. - - Args: - end_time: End time filter (ISO 8601). Defaults to now. - - start_time: Start time filter (ISO 8601). Defaults to start of current month. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/logs/summary", - body=await async_maybe_transform( - { - "end_time": end_time, - "start_time": start_time, - }, - log_get_summary_params.LogGetSummaryParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=LogGetSummaryResponse, - ) - - -class LogsResourceWithRawResponse: - def __init__(self, logs: LogsResource) -> None: - self._logs = logs - - self.create = to_raw_response_wrapper( - logs.create, - ) - self.get_summary = to_raw_response_wrapper( - logs.get_summary, - ) - - -class AsyncLogsResourceWithRawResponse: - def __init__(self, logs: AsyncLogsResource) -> None: - self._logs = logs - - self.create = async_to_raw_response_wrapper( - logs.create, - ) - self.get_summary = async_to_raw_response_wrapper( - logs.get_summary, - ) - - -class LogsResourceWithStreamingResponse: - def __init__(self, logs: LogsResource) -> None: - self._logs = logs - - self.create = to_streamed_response_wrapper( - logs.create, - ) - self.get_summary = to_streamed_response_wrapper( - logs.get_summary, - ) - - -class AsyncLogsResourceWithStreamingResponse: - def __init__(self, logs: AsyncLogsResource) -> None: - self._logs = logs - - self.create = async_to_streamed_response_wrapper( - logs.create, - ) - self.get_summary = async_to_streamed_response_wrapper( - logs.get_summary, - ) diff --git a/src/cas_parser/resources/verify_token.py b/src/cas_parser/resources/verify_token.py deleted file mode 100644 index 273eb62..0000000 --- a/src/cas_parser/resources/verify_token.py +++ /dev/null @@ -1,143 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from .._types import Body, Query, Headers, NotGiven, not_given -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.verify_token_verify_response import VerifyTokenVerifyResponse - -__all__ = ["VerifyTokenResource", "AsyncVerifyTokenResource"] - - -class VerifyTokenResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> VerifyTokenResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return VerifyTokenResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> VerifyTokenResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return VerifyTokenResourceWithStreamingResponse(self) - - def verify( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> VerifyTokenVerifyResponse: - """Verify an access token and check if it's still valid. - - Useful for debugging token - issues. - """ - return self._post( - "/v1/verify-token", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=VerifyTokenVerifyResponse, - ) - - -class AsyncVerifyTokenResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncVerifyTokenResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers - """ - return AsyncVerifyTokenResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncVerifyTokenResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response - """ - return AsyncVerifyTokenResourceWithStreamingResponse(self) - - async def verify( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> VerifyTokenVerifyResponse: - """Verify an access token and check if it's still valid. - - Useful for debugging token - issues. - """ - return await self._post( - "/v1/verify-token", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=VerifyTokenVerifyResponse, - ) - - -class VerifyTokenResourceWithRawResponse: - def __init__(self, verify_token: VerifyTokenResource) -> None: - self._verify_token = verify_token - - self.verify = to_raw_response_wrapper( - verify_token.verify, - ) - - -class AsyncVerifyTokenResourceWithRawResponse: - def __init__(self, verify_token: AsyncVerifyTokenResource) -> None: - self._verify_token = verify_token - - self.verify = async_to_raw_response_wrapper( - verify_token.verify, - ) - - -class VerifyTokenResourceWithStreamingResponse: - def __init__(self, verify_token: VerifyTokenResource) -> None: - self._verify_token = verify_token - - self.verify = to_streamed_response_wrapper( - verify_token.verify, - ) - - -class AsyncVerifyTokenResourceWithStreamingResponse: - def __init__(self, verify_token: AsyncVerifyTokenResource) -> None: - self._verify_token = verify_token - - self.verify = async_to_streamed_response_wrapper( - verify_token.verify, - ) diff --git a/src/cas_parser/types/__init__.py b/src/cas_parser/types/__init__.py index d72939a..ee5666d 100644 --- a/src/cas_parser/types/__init__.py +++ b/src/cas_parser/types/__init__.py @@ -5,24 +5,16 @@ from .transaction import Transaction as Transaction from .linked_holder import LinkedHolder as LinkedHolder from .unified_response import UnifiedResponse as UnifiedResponse -from .log_create_params import LogCreateParams as LogCreateParams from .nsdl_parse_params import NsdlParseParams as NsdlParseParams -from .log_create_response import LogCreateResponse as LogCreateResponse from .cdsl_parse_pdf_params import CdslParsePdfParams as CdslParsePdfParams -from .credit_check_response import CreditCheckResponse as CreditCheckResponse -from .log_get_summary_params import LogGetSummaryParams as LogGetSummaryParams -from .log_get_summary_response import LogGetSummaryResponse as LogGetSummaryResponse -from .access_token_create_params import AccessTokenCreateParams as AccessTokenCreateParams from .cams_kfintech_parse_params import CamsKfintechParseParams as CamsKfintechParseParams from .contract_note_parse_params import ContractNoteParseParams as ContractNoteParseParams from .inbox_connect_email_params import InboxConnectEmailParams as InboxConnectEmailParams from .smart_parse_cas_pdf_params import SmartParseCasPdfParams as SmartParseCasPdfParams from .inbox_list_cas_files_params import InboxListCasFilesParams as InboxListCasFilesParams -from .access_token_create_response import AccessTokenCreateResponse as AccessTokenCreateResponse from .contract_note_parse_response import ContractNoteParseResponse as ContractNoteParseResponse from .inbox_connect_email_response import InboxConnectEmailResponse as InboxConnectEmailResponse from .kfintech_generate_cas_params import KfintechGenerateCasParams as KfintechGenerateCasParams -from .verify_token_verify_response import VerifyTokenVerifyResponse as VerifyTokenVerifyResponse from .inbox_list_cas_files_response import InboxListCasFilesResponse as InboxListCasFilesResponse from .kfintech_generate_cas_response import KfintechGenerateCasResponse as KfintechGenerateCasResponse from .inbox_disconnect_email_response import InboxDisconnectEmailResponse as InboxDisconnectEmailResponse diff --git a/src/cas_parser/types/access_token_create_params.py b/src/cas_parser/types/access_token_create_params.py deleted file mode 100644 index 1d3f392..0000000 --- a/src/cas_parser/types/access_token_create_params.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import TypedDict - -__all__ = ["AccessTokenCreateParams"] - - -class AccessTokenCreateParams(TypedDict, total=False): - expiry_minutes: int - """Token validity in minutes (max 60)""" diff --git a/src/cas_parser/types/access_token_create_response.py b/src/cas_parser/types/access_token_create_response.py deleted file mode 100644 index 9a03c8c..0000000 --- a/src/cas_parser/types/access_token_create_response.py +++ /dev/null @@ -1,18 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["AccessTokenCreateResponse"] - - -class AccessTokenCreateResponse(BaseModel): - access_token: Optional[str] = None - """The at\\__ prefixed access token""" - - expires_in: Optional[int] = None - """Token validity in seconds""" - - token_type: Optional[str] = None - """Always "api_key" - token is a drop-in replacement for x-api-key header""" diff --git a/src/cas_parser/types/credit_check_response.py b/src/cas_parser/types/credit_check_response.py deleted file mode 100644 index 9396b9f..0000000 --- a/src/cas_parser/types/credit_check_response.py +++ /dev/null @@ -1,28 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from datetime import datetime - -from .._models import BaseModel - -__all__ = ["CreditCheckResponse"] - - -class CreditCheckResponse(BaseModel): - enabled_features: Optional[List[str]] = None - """List of API features enabled for your plan""" - - is_unlimited: Optional[bool] = None - """Whether the account has unlimited credits""" - - limit: Optional[int] = None - """Total credit limit for billing period""" - - remaining: Optional[float] = None - """Remaining credits (null if unlimited)""" - - resets_at: Optional[datetime] = None - """When credits reset (ISO 8601)""" - - used: Optional[float] = None - """Number of credits used this billing period""" diff --git a/src/cas_parser/types/log_create_params.py b/src/cas_parser/types/log_create_params.py deleted file mode 100644 index 6104297..0000000 --- a/src/cas_parser/types/log_create_params.py +++ /dev/null @@ -1,22 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Union -from datetime import datetime -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["LogCreateParams"] - - -class LogCreateParams(TypedDict, total=False): - end_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] - """End time filter (ISO 8601). Defaults to now.""" - - limit: int - """Maximum number of logs to return""" - - start_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] - """Start time filter (ISO 8601). Defaults to 30 days ago.""" diff --git a/src/cas_parser/types/log_create_response.py b/src/cas_parser/types/log_create_response.py deleted file mode 100644 index 446d6e5..0000000 --- a/src/cas_parser/types/log_create_response.py +++ /dev/null @@ -1,37 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from datetime import datetime - -from .._models import BaseModel - -__all__ = ["LogCreateResponse", "Log"] - - -class Log(BaseModel): - credits: Optional[float] = None - """Credits consumed for this request""" - - feature: Optional[str] = None - """API feature used""" - - path: Optional[str] = None - """API endpoint path""" - - request_id: Optional[str] = None - """Unique request identifier""" - - status_code: Optional[int] = None - """HTTP response status code""" - - timestamp: Optional[datetime] = None - """When the request was made""" - - -class LogCreateResponse(BaseModel): - count: Optional[int] = None - """Number of logs returned""" - - logs: Optional[List[Log]] = None - - status: Optional[str] = None diff --git a/src/cas_parser/types/log_get_summary_params.py b/src/cas_parser/types/log_get_summary_params.py deleted file mode 100644 index fc9ffe7..0000000 --- a/src/cas_parser/types/log_get_summary_params.py +++ /dev/null @@ -1,19 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Union -from datetime import datetime -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["LogGetSummaryParams"] - - -class LogGetSummaryParams(TypedDict, total=False): - end_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] - """End time filter (ISO 8601). Defaults to now.""" - - start_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] - """Start time filter (ISO 8601). Defaults to start of current month.""" diff --git a/src/cas_parser/types/log_get_summary_response.py b/src/cas_parser/types/log_get_summary_response.py deleted file mode 100644 index d947f84..0000000 --- a/src/cas_parser/types/log_get_summary_response.py +++ /dev/null @@ -1,35 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from .._models import BaseModel - -__all__ = ["LogGetSummaryResponse", "Summary", "SummaryByFeature"] - - -class SummaryByFeature(BaseModel): - credits: Optional[float] = None - """Credits consumed by this feature""" - - feature: Optional[str] = None - """API feature name""" - - requests: Optional[int] = None - """Number of requests for this feature""" - - -class Summary(BaseModel): - by_feature: Optional[List[SummaryByFeature]] = None - """Usage breakdown by feature""" - - total_credits: Optional[float] = None - """Total credits consumed in the period""" - - total_requests: Optional[int] = None - """Total API requests made in the period""" - - -class LogGetSummaryResponse(BaseModel): - status: Optional[str] = None - - summary: Optional[Summary] = None diff --git a/src/cas_parser/types/verify_token_verify_response.py b/src/cas_parser/types/verify_token_verify_response.py deleted file mode 100644 index fcfebe7..0000000 --- a/src/cas_parser/types/verify_token_verify_response.py +++ /dev/null @@ -1,18 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["VerifyTokenVerifyResponse"] - - -class VerifyTokenVerifyResponse(BaseModel): - error: Optional[str] = None - """Error message (only shown if invalid)""" - - masked_api_key: Optional[str] = None - """Masked API key (only shown if valid)""" - - valid: Optional[bool] = None - """Whether the token is valid""" diff --git a/tests/api_resources/test_access_token.py b/tests/api_resources/test_access_token.py deleted file mode 100644 index 32d63c9..0000000 --- a/tests/api_resources/test_access_token.py +++ /dev/null @@ -1,96 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from cas_parser import CasParser, AsyncCasParser -from tests.utils import assert_matches_type -from cas_parser.types import AccessTokenCreateResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestAccessToken: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_create(self, client: CasParser) -> None: - access_token = client.access_token.create() - assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: CasParser) -> None: - access_token = client.access_token.create( - expiry_minutes=60, - ) - assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_create(self, client: CasParser) -> None: - response = client.access_token.with_raw_response.create() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - access_token = response.parse() - assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_create(self, client: CasParser) -> None: - with client.access_token.with_streaming_response.create() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - access_token = response.parse() - assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncAccessToken: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncCasParser) -> None: - access_token = await async_client.access_token.create() - assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncCasParser) -> None: - access_token = await async_client.access_token.create( - expiry_minutes=60, - ) - assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: - response = await async_client.access_token.with_raw_response.create() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - access_token = await response.parse() - assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncCasParser) -> None: - async with async_client.access_token.with_streaming_response.create() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - access_token = await response.parse() - assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_credits.py b/tests/api_resources/test_credits.py deleted file mode 100644 index 8538889..0000000 --- a/tests/api_resources/test_credits.py +++ /dev/null @@ -1,80 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from cas_parser import CasParser, AsyncCasParser -from tests.utils import assert_matches_type -from cas_parser.types import CreditCheckResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestCredits: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_check(self, client: CasParser) -> None: - credit = client.credits.check() - assert_matches_type(CreditCheckResponse, credit, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_check(self, client: CasParser) -> None: - response = client.credits.with_raw_response.check() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - credit = response.parse() - assert_matches_type(CreditCheckResponse, credit, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_check(self, client: CasParser) -> None: - with client.credits.with_streaming_response.check() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - credit = response.parse() - assert_matches_type(CreditCheckResponse, credit, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncCredits: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_check(self, async_client: AsyncCasParser) -> None: - credit = await async_client.credits.check() - assert_matches_type(CreditCheckResponse, credit, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_check(self, async_client: AsyncCasParser) -> None: - response = await async_client.credits.with_raw_response.check() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - credit = await response.parse() - assert_matches_type(CreditCheckResponse, credit, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_check(self, async_client: AsyncCasParser) -> None: - async with async_client.credits.with_streaming_response.check() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - credit = await response.parse() - assert_matches_type(CreditCheckResponse, credit, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_logs.py b/tests/api_resources/test_logs.py deleted file mode 100644 index 43908ef..0000000 --- a/tests/api_resources/test_logs.py +++ /dev/null @@ -1,175 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from cas_parser import CasParser, AsyncCasParser -from tests.utils import assert_matches_type -from cas_parser.types import LogCreateResponse, LogGetSummaryResponse -from cas_parser._utils import parse_datetime - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestLogs: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_create(self, client: CasParser) -> None: - log = client.logs.create() - assert_matches_type(LogCreateResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: CasParser) -> None: - log = client.logs.create( - end_time=parse_datetime("2026-01-31T23:59:59Z"), - limit=1, - start_time=parse_datetime("2026-01-01T00:00:00Z"), - ) - assert_matches_type(LogCreateResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_create(self, client: CasParser) -> None: - response = client.logs.with_raw_response.create() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - log = response.parse() - assert_matches_type(LogCreateResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_create(self, client: CasParser) -> None: - with client.logs.with_streaming_response.create() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - log = response.parse() - assert_matches_type(LogCreateResponse, log, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_get_summary(self, client: CasParser) -> None: - log = client.logs.get_summary() - assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_get_summary_with_all_params(self, client: CasParser) -> None: - log = client.logs.get_summary( - end_time=parse_datetime("2019-12-27T18:11:19.117Z"), - start_time=parse_datetime("2019-12-27T18:11:19.117Z"), - ) - assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_get_summary(self, client: CasParser) -> None: - response = client.logs.with_raw_response.get_summary() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - log = response.parse() - assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_get_summary(self, client: CasParser) -> None: - with client.logs.with_streaming_response.get_summary() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - log = response.parse() - assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncLogs: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncCasParser) -> None: - log = await async_client.logs.create() - assert_matches_type(LogCreateResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncCasParser) -> None: - log = await async_client.logs.create( - end_time=parse_datetime("2026-01-31T23:59:59Z"), - limit=1, - start_time=parse_datetime("2026-01-01T00:00:00Z"), - ) - assert_matches_type(LogCreateResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: - response = await async_client.logs.with_raw_response.create() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - log = await response.parse() - assert_matches_type(LogCreateResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncCasParser) -> None: - async with async_client.logs.with_streaming_response.create() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - log = await response.parse() - assert_matches_type(LogCreateResponse, log, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_get_summary(self, async_client: AsyncCasParser) -> None: - log = await async_client.logs.get_summary() - assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_get_summary_with_all_params(self, async_client: AsyncCasParser) -> None: - log = await async_client.logs.get_summary( - end_time=parse_datetime("2019-12-27T18:11:19.117Z"), - start_time=parse_datetime("2019-12-27T18:11:19.117Z"), - ) - assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_get_summary(self, async_client: AsyncCasParser) -> None: - response = await async_client.logs.with_raw_response.get_summary() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - log = await response.parse() - assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_get_summary(self, async_client: AsyncCasParser) -> None: - async with async_client.logs.with_streaming_response.get_summary() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - log = await response.parse() - assert_matches_type(LogGetSummaryResponse, log, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_verify_token.py b/tests/api_resources/test_verify_token.py deleted file mode 100644 index 43e594d..0000000 --- a/tests/api_resources/test_verify_token.py +++ /dev/null @@ -1,80 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from cas_parser import CasParser, AsyncCasParser -from tests.utils import assert_matches_type -from cas_parser.types import VerifyTokenVerifyResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestVerifyToken: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_verify(self, client: CasParser) -> None: - verify_token = client.verify_token.verify() - assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_verify(self, client: CasParser) -> None: - response = client.verify_token.with_raw_response.verify() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - verify_token = response.parse() - assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_verify(self, client: CasParser) -> None: - with client.verify_token.with_streaming_response.verify() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - verify_token = response.parse() - assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncVerifyToken: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_verify(self, async_client: AsyncCasParser) -> None: - verify_token = await async_client.verify_token.verify() - assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_verify(self, async_client: AsyncCasParser) -> None: - response = await async_client.verify_token.with_raw_response.verify() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - verify_token = await response.parse() - assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_verify(self, async_client: AsyncCasParser) -> None: - async with async_client.verify_token.with_streaming_response.verify() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - verify_token = await response.parse() - assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index ef1fd91..ed6cfac 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -863,20 +863,20 @@ def test_parse_retry_after_header( @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: CasParser) -> None: - respx_mock.post("/credits").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.credits.with_streaming_response.check().__enter__() + client.cams_kfintech.with_streaming_response.parse().__enter__() assert _get_open_connections(client) == 0 @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: CasParser) -> None: - respx_mock.post("/credits").mock(return_value=httpx.Response(500)) + respx_mock.post("/v4/cams_kfintech/parse").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.credits.with_streaming_response.check().__enter__() + client.cams_kfintech.with_streaming_response.parse().__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -903,9 +903,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/credits").mock(side_effect=retry_handler) + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) - response = client.credits.with_raw_response.check() + response = client.cams_kfintech.with_raw_response.parse() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -927,9 +927,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/credits").mock(side_effect=retry_handler) + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) - response = client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": Omit()}) + response = client.cams_kfintech.with_raw_response.parse(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -950,9 +950,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/credits").mock(side_effect=retry_handler) + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) - response = client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": "42"}) + response = client.cams_kfintech.with_raw_response.parse(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1775,10 +1775,10 @@ async def test_parse_retry_after_header( async def test_retrying_timeout_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncCasParser ) -> None: - respx_mock.post("/credits").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.credits.with_streaming_response.check().__aenter__() + await async_client.cams_kfintech.with_streaming_response.parse().__aenter__() assert _get_open_connections(async_client) == 0 @@ -1787,10 +1787,10 @@ async def test_retrying_timeout_errors_doesnt_leak( async def test_retrying_status_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncCasParser ) -> None: - respx_mock.post("/credits").mock(return_value=httpx.Response(500)) + respx_mock.post("/v4/cams_kfintech/parse").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.credits.with_streaming_response.check().__aenter__() + await async_client.cams_kfintech.with_streaming_response.parse().__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1817,9 +1817,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/credits").mock(side_effect=retry_handler) + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) - response = await client.credits.with_raw_response.check() + response = await client.cams_kfintech.with_raw_response.parse() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1841,9 +1841,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/credits").mock(side_effect=retry_handler) + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) - response = await client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": Omit()}) + response = await client.cams_kfintech.with_raw_response.parse(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1864,9 +1864,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/credits").mock(side_effect=retry_handler) + respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) - response = await client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": "42"}) + response = await client.cams_kfintech.with_raw_response.parse(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 53c95ccfe3221c48a4dd978092520e150dbf39c1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:17:54 +0000 Subject: [PATCH 070/116] feat(api): api update --- .stats.yml | 4 ++-- src/cas_parser/types/inbox_list_cas_files_response.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 80a6900..1e5b8d1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 12 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-6a9d3b677dcfb856dc571865c34b3fe401e4d7f0d799edfc743acb9a55800bd0.yml -openapi_spec_hash: 037703a6c741e4310fda3f57c22fa51e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-e6762e83ef7cdff129d03d0ab8c130db2fb5d1d820142847c27d72b40a0e9f53.yml +openapi_spec_hash: f38fb40a2b28bae4b0c9c4228c1c0e0d config_hash: 41c337f5cda03b13880617490f82bad0 diff --git a/src/cas_parser/types/inbox_list_cas_files_response.py b/src/cas_parser/types/inbox_list_cas_files_response.py index ef27d92..686944a 100644 --- a/src/cas_parser/types/inbox_list_cas_files_response.py +++ b/src/cas_parser/types/inbox_list_cas_files_response.py @@ -30,6 +30,9 @@ class File(BaseModel): original_filename: Optional[str] = None """Original attachment filename from the email""" + sender_email: Optional[str] = None + """Email address of the CAS authority who sent this""" + size: Optional[int] = None """File size in bytes""" From 2b8837a5a5d258d245241d9cee8e06860f189c11 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:17:44 +0000 Subject: [PATCH 071/116] feat(api): api update --- .stats.yml | 4 ++-- src/cas_parser/types/inbox_list_cas_files_response.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1e5b8d1..b880d0c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 12 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-e6762e83ef7cdff129d03d0ab8c130db2fb5d1d820142847c27d72b40a0e9f53.yml -openapi_spec_hash: f38fb40a2b28bae4b0c9c4228c1c0e0d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-d9763d006969b49a1473851069fdfa429eb13133b64103a62963bb70ddb22305.yml +openapi_spec_hash: 6aee689b7a759b12c85c088c15e29bc0 config_hash: 41c337f5cda03b13880617490f82bad0 diff --git a/src/cas_parser/types/inbox_list_cas_files_response.py b/src/cas_parser/types/inbox_list_cas_files_response.py index 686944a..25b960f 100644 --- a/src/cas_parser/types/inbox_list_cas_files_response.py +++ b/src/cas_parser/types/inbox_list_cas_files_response.py @@ -31,7 +31,10 @@ class File(BaseModel): """Original attachment filename from the email""" sender_email: Optional[str] = None - """Email address of the CAS authority who sent this""" + """ + Email address of the CAS authority (CDSL, NSDL, CAMS, or KFintech) who + originally sent this statement + """ size: Optional[int] = None """File size in bytes""" From cbdad303fd47597696ae63b8eba4a1db6cbe3c8e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:53:25 +0000 Subject: [PATCH 072/116] feat(api): manual updates --- .stats.yml | 4 +- README.md | 26 +- api.md | 69 +++ src/cas_parser/_client.py | 200 ++++++- src/cas_parser/resources/__init__.py | 70 +++ src/cas_parser/resources/access_token.py | 195 ++++++ src/cas_parser/resources/credits.py | 155 +++++ src/cas_parser/resources/inbound_email.py | 563 ++++++++++++++++++ src/cas_parser/resources/logs.py | 315 ++++++++++ src/cas_parser/resources/verify_token.py | 143 +++++ src/cas_parser/types/__init__.py | 14 + .../types/access_token_create_params.py | 12 + .../types/access_token_create_response.py | 18 + src/cas_parser/types/credit_check_response.py | 28 + .../types/inbound_email_create_params.py | 47 ++ .../types/inbound_email_create_response.py | 40 ++ .../types/inbound_email_delete_response.py | 13 + .../types/inbound_email_list_params.py | 18 + .../types/inbound_email_list_response.py | 53 ++ .../types/inbound_email_retrieve_response.py | 40 ++ src/cas_parser/types/log_create_params.py | 22 + src/cas_parser/types/log_create_response.py | 37 ++ .../types/log_get_summary_params.py | 19 + .../types/log_get_summary_response.py | 35 ++ .../types/verify_token_verify_response.py | 18 + tests/api_resources/test_access_token.py | 96 +++ tests/api_resources/test_credits.py | 80 +++ tests/api_resources/test_inbound_email.py | 371 ++++++++++++ tests/api_resources/test_logs.py | 175 ++++++ tests/api_resources/test_verify_token.py | 80 +++ tests/test_client.py | 40 +- 31 files changed, 2960 insertions(+), 36 deletions(-) create mode 100644 src/cas_parser/resources/access_token.py create mode 100644 src/cas_parser/resources/credits.py create mode 100644 src/cas_parser/resources/inbound_email.py create mode 100644 src/cas_parser/resources/logs.py create mode 100644 src/cas_parser/resources/verify_token.py create mode 100644 src/cas_parser/types/access_token_create_params.py create mode 100644 src/cas_parser/types/access_token_create_response.py create mode 100644 src/cas_parser/types/credit_check_response.py create mode 100644 src/cas_parser/types/inbound_email_create_params.py create mode 100644 src/cas_parser/types/inbound_email_create_response.py create mode 100644 src/cas_parser/types/inbound_email_delete_response.py create mode 100644 src/cas_parser/types/inbound_email_list_params.py create mode 100644 src/cas_parser/types/inbound_email_list_response.py create mode 100644 src/cas_parser/types/inbound_email_retrieve_response.py create mode 100644 src/cas_parser/types/log_create_params.py create mode 100644 src/cas_parser/types/log_create_response.py create mode 100644 src/cas_parser/types/log_get_summary_params.py create mode 100644 src/cas_parser/types/log_get_summary_response.py create mode 100644 src/cas_parser/types/verify_token_verify_response.py create mode 100644 tests/api_resources/test_access_token.py create mode 100644 tests/api_resources/test_credits.py create mode 100644 tests/api_resources/test_inbound_email.py create mode 100644 tests/api_resources/test_logs.py create mode 100644 tests/api_resources/test_verify_token.py diff --git a/.stats.yml b/.stats.yml index b880d0c..cdf3c0a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 12 +configured_endpoints: 21 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-d9763d006969b49a1473851069fdfa429eb13133b64103a62963bb70ddb22305.yml openapi_spec_hash: 6aee689b7a759b12c85c088c15e29bc0 -config_hash: 41c337f5cda03b13880617490f82bad0 +config_hash: d54f39abb185904495bef7c5f8702746 diff --git a/README.md b/README.md index 04c0f3e..f7b645a 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ client = CasParser( environment="environment_1", ) -unified_response = client.cams_kfintech.parse() -print(unified_response.demat_accounts) +response = client.credits.check() +print(response.enabled_features) ``` While you can provide an `api_key` keyword argument, @@ -69,8 +69,8 @@ client = AsyncCasParser( async def main() -> None: - unified_response = await client.cams_kfintech.parse() - print(unified_response.demat_accounts) + response = await client.credits.check() + print(response.enabled_features) asyncio.run(main()) @@ -103,8 +103,8 @@ async def main() -> None: api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: - unified_response = await client.cams_kfintech.parse() - print(unified_response.demat_accounts) + response = await client.credits.check() + print(response.enabled_features) asyncio.run(main()) @@ -135,7 +135,7 @@ from cas_parser import CasParser client = CasParser() try: - client.cams_kfintech.parse() + client.credits.check() except cas_parser.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. @@ -178,7 +178,7 @@ client = CasParser( ) # Or, configure per-request: -client.with_options(max_retries=5).cams_kfintech.parse() +client.with_options(max_retries=5).credits.check() ``` ### Timeouts @@ -201,7 +201,7 @@ client = CasParser( ) # Override per-request: -client.with_options(timeout=5.0).cams_kfintech.parse() +client.with_options(timeout=5.0).credits.check() ``` On timeout, an `APITimeoutError` is thrown. @@ -242,11 +242,11 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from cas_parser import CasParser client = CasParser() -response = client.cams_kfintech.with_raw_response.parse() +response = client.credits.with_raw_response.check() print(response.headers.get('X-My-Header')) -cams_kfintech = response.parse() # get the object that `cams_kfintech.parse()` would have returned -print(cams_kfintech.demat_accounts) +credit = response.parse() # get the object that `credits.check()` would have returned +print(credit.enabled_features) ``` These methods return an [`APIResponse`](https://github.com/CASParser/cas-parser-python/tree/main/src/cas_parser/_response.py) object. @@ -260,7 +260,7 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.cams_kfintech.with_streaming_response.parse() as response: +with client.credits.with_streaming_response.check() as response: print(response.headers.get("X-My-Header")) for line in response.iter_lines(): diff --git a/api.md b/api.md index 8ba2d2c..164b61d 100644 --- a/api.md +++ b/api.md @@ -1,3 +1,52 @@ +# Credits + +Types: + +```python +from cas_parser.types import CreditCheckResponse +``` + +Methods: + +- client.credits.check() -> CreditCheckResponse + +# Logs + +Types: + +```python +from cas_parser.types import LogCreateResponse, LogGetSummaryResponse +``` + +Methods: + +- client.logs.create(\*\*params) -> LogCreateResponse +- client.logs.get_summary(\*\*params) -> LogGetSummaryResponse + +# AccessToken + +Types: + +```python +from cas_parser.types import AccessTokenCreateResponse +``` + +Methods: + +- client.access_token.create(\*\*params) -> AccessTokenCreateResponse + +# VerifyToken + +Types: + +```python +from cas_parser.types import VerifyTokenVerifyResponse +``` + +Methods: + +- client.verify_token.verify() -> VerifyTokenVerifyResponse + # CamsKfintech Types: @@ -84,3 +133,23 @@ Methods: Methods: - client.smart.parse_cas_pdf(\*\*params) -> UnifiedResponse + +# InboundEmail + +Types: + +```python +from cas_parser.types import ( + InboundEmailCreateResponse, + InboundEmailRetrieveResponse, + InboundEmailListResponse, + InboundEmailDeleteResponse, +) +``` + +Methods: + +- client.inbound_email.create(\*\*params) -> InboundEmailCreateResponse +- client.inbound_email.retrieve(inbound_email_id) -> InboundEmailRetrieveResponse +- client.inbound_email.list(\*\*params) -> InboundEmailListResponse +- client.inbound_email.delete(inbound_email_id) -> InboundEmailDeleteResponse diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py index 8041bb8..e63f97f 100644 --- a/src/cas_parser/_client.py +++ b/src/cas_parser/_client.py @@ -31,14 +31,32 @@ ) if TYPE_CHECKING: - from .resources import cdsl, nsdl, inbox, smart, kfintech, cams_kfintech, contract_note + from .resources import ( + cdsl, + logs, + nsdl, + inbox, + smart, + credits, + kfintech, + access_token, + verify_token, + cams_kfintech, + contract_note, + inbound_email, + ) + from .resources.logs import LogsResource, AsyncLogsResource from .resources.nsdl import NsdlResource, AsyncNsdlResource from .resources.inbox import InboxResource, AsyncInboxResource from .resources.smart import SmartResource, AsyncSmartResource + from .resources.credits import CreditsResource, AsyncCreditsResource from .resources.kfintech import KfintechResource, AsyncKfintechResource from .resources.cdsl.cdsl import CdslResource, AsyncCdslResource + from .resources.access_token import AccessTokenResource, AsyncAccessTokenResource + from .resources.verify_token import VerifyTokenResource, AsyncVerifyTokenResource from .resources.cams_kfintech import CamsKfintechResource, AsyncCamsKfintechResource from .resources.contract_note import ContractNoteResource, AsyncContractNoteResource + from .resources.inbound_email import InboundEmailResource, AsyncInboundEmailResource __all__ = [ "ENVIRONMENTS", @@ -138,6 +156,30 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + @cached_property + def credits(self) -> CreditsResource: + from .resources.credits import CreditsResource + + return CreditsResource(self) + + @cached_property + def logs(self) -> LogsResource: + from .resources.logs import LogsResource + + return LogsResource(self) + + @cached_property + def access_token(self) -> AccessTokenResource: + from .resources.access_token import AccessTokenResource + + return AccessTokenResource(self) + + @cached_property + def verify_token(self) -> VerifyTokenResource: + from .resources.verify_token import VerifyTokenResource + + return VerifyTokenResource(self) + @cached_property def cams_kfintech(self) -> CamsKfintechResource: from .resources.cams_kfintech import CamsKfintechResource @@ -180,6 +222,12 @@ def smart(self) -> SmartResource: return SmartResource(self) + @cached_property + def inbound_email(self) -> InboundEmailResource: + from .resources.inbound_email import InboundEmailResource + + return InboundEmailResource(self) + @cached_property def with_raw_response(self) -> CasParserWithRawResponse: return CasParserWithRawResponse(self) @@ -374,6 +422,30 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + @cached_property + def credits(self) -> AsyncCreditsResource: + from .resources.credits import AsyncCreditsResource + + return AsyncCreditsResource(self) + + @cached_property + def logs(self) -> AsyncLogsResource: + from .resources.logs import AsyncLogsResource + + return AsyncLogsResource(self) + + @cached_property + def access_token(self) -> AsyncAccessTokenResource: + from .resources.access_token import AsyncAccessTokenResource + + return AsyncAccessTokenResource(self) + + @cached_property + def verify_token(self) -> AsyncVerifyTokenResource: + from .resources.verify_token import AsyncVerifyTokenResource + + return AsyncVerifyTokenResource(self) + @cached_property def cams_kfintech(self) -> AsyncCamsKfintechResource: from .resources.cams_kfintech import AsyncCamsKfintechResource @@ -416,6 +488,12 @@ def smart(self) -> AsyncSmartResource: return AsyncSmartResource(self) + @cached_property + def inbound_email(self) -> AsyncInboundEmailResource: + from .resources.inbound_email import AsyncInboundEmailResource + + return AsyncInboundEmailResource(self) + @cached_property def with_raw_response(self) -> AsyncCasParserWithRawResponse: return AsyncCasParserWithRawResponse(self) @@ -537,6 +615,30 @@ class CasParserWithRawResponse: def __init__(self, client: CasParser) -> None: self._client = client + @cached_property + def credits(self) -> credits.CreditsResourceWithRawResponse: + from .resources.credits import CreditsResourceWithRawResponse + + return CreditsResourceWithRawResponse(self._client.credits) + + @cached_property + def logs(self) -> logs.LogsResourceWithRawResponse: + from .resources.logs import LogsResourceWithRawResponse + + return LogsResourceWithRawResponse(self._client.logs) + + @cached_property + def access_token(self) -> access_token.AccessTokenResourceWithRawResponse: + from .resources.access_token import AccessTokenResourceWithRawResponse + + return AccessTokenResourceWithRawResponse(self._client.access_token) + + @cached_property + def verify_token(self) -> verify_token.VerifyTokenResourceWithRawResponse: + from .resources.verify_token import VerifyTokenResourceWithRawResponse + + return VerifyTokenResourceWithRawResponse(self._client.verify_token) + @cached_property def cams_kfintech(self) -> cams_kfintech.CamsKfintechResourceWithRawResponse: from .resources.cams_kfintech import CamsKfintechResourceWithRawResponse @@ -579,6 +681,12 @@ def smart(self) -> smart.SmartResourceWithRawResponse: return SmartResourceWithRawResponse(self._client.smart) + @cached_property + def inbound_email(self) -> inbound_email.InboundEmailResourceWithRawResponse: + from .resources.inbound_email import InboundEmailResourceWithRawResponse + + return InboundEmailResourceWithRawResponse(self._client.inbound_email) + class AsyncCasParserWithRawResponse: _client: AsyncCasParser @@ -586,6 +694,30 @@ class AsyncCasParserWithRawResponse: def __init__(self, client: AsyncCasParser) -> None: self._client = client + @cached_property + def credits(self) -> credits.AsyncCreditsResourceWithRawResponse: + from .resources.credits import AsyncCreditsResourceWithRawResponse + + return AsyncCreditsResourceWithRawResponse(self._client.credits) + + @cached_property + def logs(self) -> logs.AsyncLogsResourceWithRawResponse: + from .resources.logs import AsyncLogsResourceWithRawResponse + + return AsyncLogsResourceWithRawResponse(self._client.logs) + + @cached_property + def access_token(self) -> access_token.AsyncAccessTokenResourceWithRawResponse: + from .resources.access_token import AsyncAccessTokenResourceWithRawResponse + + return AsyncAccessTokenResourceWithRawResponse(self._client.access_token) + + @cached_property + def verify_token(self) -> verify_token.AsyncVerifyTokenResourceWithRawResponse: + from .resources.verify_token import AsyncVerifyTokenResourceWithRawResponse + + return AsyncVerifyTokenResourceWithRawResponse(self._client.verify_token) + @cached_property def cams_kfintech(self) -> cams_kfintech.AsyncCamsKfintechResourceWithRawResponse: from .resources.cams_kfintech import AsyncCamsKfintechResourceWithRawResponse @@ -628,6 +760,12 @@ def smart(self) -> smart.AsyncSmartResourceWithRawResponse: return AsyncSmartResourceWithRawResponse(self._client.smart) + @cached_property + def inbound_email(self) -> inbound_email.AsyncInboundEmailResourceWithRawResponse: + from .resources.inbound_email import AsyncInboundEmailResourceWithRawResponse + + return AsyncInboundEmailResourceWithRawResponse(self._client.inbound_email) + class CasParserWithStreamedResponse: _client: CasParser @@ -635,6 +773,30 @@ class CasParserWithStreamedResponse: def __init__(self, client: CasParser) -> None: self._client = client + @cached_property + def credits(self) -> credits.CreditsResourceWithStreamingResponse: + from .resources.credits import CreditsResourceWithStreamingResponse + + return CreditsResourceWithStreamingResponse(self._client.credits) + + @cached_property + def logs(self) -> logs.LogsResourceWithStreamingResponse: + from .resources.logs import LogsResourceWithStreamingResponse + + return LogsResourceWithStreamingResponse(self._client.logs) + + @cached_property + def access_token(self) -> access_token.AccessTokenResourceWithStreamingResponse: + from .resources.access_token import AccessTokenResourceWithStreamingResponse + + return AccessTokenResourceWithStreamingResponse(self._client.access_token) + + @cached_property + def verify_token(self) -> verify_token.VerifyTokenResourceWithStreamingResponse: + from .resources.verify_token import VerifyTokenResourceWithStreamingResponse + + return VerifyTokenResourceWithStreamingResponse(self._client.verify_token) + @cached_property def cams_kfintech(self) -> cams_kfintech.CamsKfintechResourceWithStreamingResponse: from .resources.cams_kfintech import CamsKfintechResourceWithStreamingResponse @@ -677,6 +839,12 @@ def smart(self) -> smart.SmartResourceWithStreamingResponse: return SmartResourceWithStreamingResponse(self._client.smart) + @cached_property + def inbound_email(self) -> inbound_email.InboundEmailResourceWithStreamingResponse: + from .resources.inbound_email import InboundEmailResourceWithStreamingResponse + + return InboundEmailResourceWithStreamingResponse(self._client.inbound_email) + class AsyncCasParserWithStreamedResponse: _client: AsyncCasParser @@ -684,6 +852,30 @@ class AsyncCasParserWithStreamedResponse: def __init__(self, client: AsyncCasParser) -> None: self._client = client + @cached_property + def credits(self) -> credits.AsyncCreditsResourceWithStreamingResponse: + from .resources.credits import AsyncCreditsResourceWithStreamingResponse + + return AsyncCreditsResourceWithStreamingResponse(self._client.credits) + + @cached_property + def logs(self) -> logs.AsyncLogsResourceWithStreamingResponse: + from .resources.logs import AsyncLogsResourceWithStreamingResponse + + return AsyncLogsResourceWithStreamingResponse(self._client.logs) + + @cached_property + def access_token(self) -> access_token.AsyncAccessTokenResourceWithStreamingResponse: + from .resources.access_token import AsyncAccessTokenResourceWithStreamingResponse + + return AsyncAccessTokenResourceWithStreamingResponse(self._client.access_token) + + @cached_property + def verify_token(self) -> verify_token.AsyncVerifyTokenResourceWithStreamingResponse: + from .resources.verify_token import AsyncVerifyTokenResourceWithStreamingResponse + + return AsyncVerifyTokenResourceWithStreamingResponse(self._client.verify_token) + @cached_property def cams_kfintech(self) -> cams_kfintech.AsyncCamsKfintechResourceWithStreamingResponse: from .resources.cams_kfintech import AsyncCamsKfintechResourceWithStreamingResponse @@ -726,6 +918,12 @@ def smart(self) -> smart.AsyncSmartResourceWithStreamingResponse: return AsyncSmartResourceWithStreamingResponse(self._client.smart) + @cached_property + def inbound_email(self) -> inbound_email.AsyncInboundEmailResourceWithStreamingResponse: + from .resources.inbound_email import AsyncInboundEmailResourceWithStreamingResponse + + return AsyncInboundEmailResourceWithStreamingResponse(self._client.inbound_email) + Client = CasParser diff --git a/src/cas_parser/resources/__init__.py b/src/cas_parser/resources/__init__.py index 4125404..34f05f7 100644 --- a/src/cas_parser/resources/__init__.py +++ b/src/cas_parser/resources/__init__.py @@ -8,6 +8,14 @@ CdslResourceWithStreamingResponse, AsyncCdslResourceWithStreamingResponse, ) +from .logs import ( + LogsResource, + AsyncLogsResource, + LogsResourceWithRawResponse, + AsyncLogsResourceWithRawResponse, + LogsResourceWithStreamingResponse, + AsyncLogsResourceWithStreamingResponse, +) from .nsdl import ( NsdlResource, AsyncNsdlResource, @@ -32,6 +40,14 @@ SmartResourceWithStreamingResponse, AsyncSmartResourceWithStreamingResponse, ) +from .credits import ( + CreditsResource, + AsyncCreditsResource, + CreditsResourceWithRawResponse, + AsyncCreditsResourceWithRawResponse, + CreditsResourceWithStreamingResponse, + AsyncCreditsResourceWithStreamingResponse, +) from .kfintech import ( KfintechResource, AsyncKfintechResource, @@ -40,6 +56,22 @@ KfintechResourceWithStreamingResponse, AsyncKfintechResourceWithStreamingResponse, ) +from .access_token import ( + AccessTokenResource, + AsyncAccessTokenResource, + AccessTokenResourceWithRawResponse, + AsyncAccessTokenResourceWithRawResponse, + AccessTokenResourceWithStreamingResponse, + AsyncAccessTokenResourceWithStreamingResponse, +) +from .verify_token import ( + VerifyTokenResource, + AsyncVerifyTokenResource, + VerifyTokenResourceWithRawResponse, + AsyncVerifyTokenResourceWithRawResponse, + VerifyTokenResourceWithStreamingResponse, + AsyncVerifyTokenResourceWithStreamingResponse, +) from .cams_kfintech import ( CamsKfintechResource, AsyncCamsKfintechResource, @@ -56,8 +88,40 @@ ContractNoteResourceWithStreamingResponse, AsyncContractNoteResourceWithStreamingResponse, ) +from .inbound_email import ( + InboundEmailResource, + AsyncInboundEmailResource, + InboundEmailResourceWithRawResponse, + AsyncInboundEmailResourceWithRawResponse, + InboundEmailResourceWithStreamingResponse, + AsyncInboundEmailResourceWithStreamingResponse, +) __all__ = [ + "CreditsResource", + "AsyncCreditsResource", + "CreditsResourceWithRawResponse", + "AsyncCreditsResourceWithRawResponse", + "CreditsResourceWithStreamingResponse", + "AsyncCreditsResourceWithStreamingResponse", + "LogsResource", + "AsyncLogsResource", + "LogsResourceWithRawResponse", + "AsyncLogsResourceWithRawResponse", + "LogsResourceWithStreamingResponse", + "AsyncLogsResourceWithStreamingResponse", + "AccessTokenResource", + "AsyncAccessTokenResource", + "AccessTokenResourceWithRawResponse", + "AsyncAccessTokenResourceWithRawResponse", + "AccessTokenResourceWithStreamingResponse", + "AsyncAccessTokenResourceWithStreamingResponse", + "VerifyTokenResource", + "AsyncVerifyTokenResource", + "VerifyTokenResourceWithRawResponse", + "AsyncVerifyTokenResourceWithRawResponse", + "VerifyTokenResourceWithStreamingResponse", + "AsyncVerifyTokenResourceWithStreamingResponse", "CamsKfintechResource", "AsyncCamsKfintechResource", "CamsKfintechResourceWithRawResponse", @@ -100,4 +164,10 @@ "AsyncSmartResourceWithRawResponse", "SmartResourceWithStreamingResponse", "AsyncSmartResourceWithStreamingResponse", + "InboundEmailResource", + "AsyncInboundEmailResource", + "InboundEmailResourceWithRawResponse", + "AsyncInboundEmailResourceWithRawResponse", + "InboundEmailResourceWithStreamingResponse", + "AsyncInboundEmailResourceWithStreamingResponse", ] diff --git a/src/cas_parser/resources/access_token.py b/src/cas_parser/resources/access_token.py new file mode 100644 index 0000000..bfda93b --- /dev/null +++ b/src/cas_parser/resources/access_token.py @@ -0,0 +1,195 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import access_token_create_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.access_token_create_response import AccessTokenCreateResponse + +__all__ = ["AccessTokenResource", "AsyncAccessTokenResource"] + + +class AccessTokenResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> AccessTokenResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AccessTokenResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AccessTokenResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response + """ + return AccessTokenResourceWithStreamingResponse(self) + + def create( + self, + *, + expiry_minutes: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AccessTokenCreateResponse: + """ + Generate a short-lived access token from your API key. + + **Use this endpoint from your backend** to create tokens that can be safely + passed to frontend/SDK. + + **Legacy path:** `/v1/access-token` (still supported) + + Access tokens: + + - Are prefixed with `at_` for easy identification + - Valid for up to 60 minutes + - Can be used in place of API keys on all v4 endpoints + - Cannot be used to generate other access tokens + + Args: + expiry_minutes: Token validity in minutes (max 60) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/token", + body=maybe_transform( + {"expiry_minutes": expiry_minutes}, access_token_create_params.AccessTokenCreateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AccessTokenCreateResponse, + ) + + +class AsyncAccessTokenResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncAccessTokenResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncAccessTokenResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAccessTokenResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response + """ + return AsyncAccessTokenResourceWithStreamingResponse(self) + + async def create( + self, + *, + expiry_minutes: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AccessTokenCreateResponse: + """ + Generate a short-lived access token from your API key. + + **Use this endpoint from your backend** to create tokens that can be safely + passed to frontend/SDK. + + **Legacy path:** `/v1/access-token` (still supported) + + Access tokens: + + - Are prefixed with `at_` for easy identification + - Valid for up to 60 minutes + - Can be used in place of API keys on all v4 endpoints + - Cannot be used to generate other access tokens + + Args: + expiry_minutes: Token validity in minutes (max 60) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/token", + body=await async_maybe_transform( + {"expiry_minutes": expiry_minutes}, access_token_create_params.AccessTokenCreateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AccessTokenCreateResponse, + ) + + +class AccessTokenResourceWithRawResponse: + def __init__(self, access_token: AccessTokenResource) -> None: + self._access_token = access_token + + self.create = to_raw_response_wrapper( + access_token.create, + ) + + +class AsyncAccessTokenResourceWithRawResponse: + def __init__(self, access_token: AsyncAccessTokenResource) -> None: + self._access_token = access_token + + self.create = async_to_raw_response_wrapper( + access_token.create, + ) + + +class AccessTokenResourceWithStreamingResponse: + def __init__(self, access_token: AccessTokenResource) -> None: + self._access_token = access_token + + self.create = to_streamed_response_wrapper( + access_token.create, + ) + + +class AsyncAccessTokenResourceWithStreamingResponse: + def __init__(self, access_token: AsyncAccessTokenResource) -> None: + self._access_token = access_token + + self.create = async_to_streamed_response_wrapper( + access_token.create, + ) diff --git a/src/cas_parser/resources/credits.py b/src/cas_parser/resources/credits.py new file mode 100644 index 0000000..264693d --- /dev/null +++ b/src/cas_parser/resources/credits.py @@ -0,0 +1,155 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .._types import Body, Query, Headers, NotGiven, not_given +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.credit_check_response import CreditCheckResponse + +__all__ = ["CreditsResource", "AsyncCreditsResource"] + + +class CreditsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CreditsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return CreditsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CreditsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response + """ + return CreditsResourceWithStreamingResponse(self) + + def check( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CreditCheckResponse: + """ + Check your remaining API credits and usage for the current billing period. + + Returns: + + - Number of API calls used and remaining credits + - Credit limit and reset date + - List of enabled features for your plan + + Credits reset at the start of each billing period. + """ + return self._post( + "/v1/credits", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CreditCheckResponse, + ) + + +class AsyncCreditsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCreditsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncCreditsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCreditsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response + """ + return AsyncCreditsResourceWithStreamingResponse(self) + + async def check( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> CreditCheckResponse: + """ + Check your remaining API credits and usage for the current billing period. + + Returns: + + - Number of API calls used and remaining credits + - Credit limit and reset date + - List of enabled features for your plan + + Credits reset at the start of each billing period. + """ + return await self._post( + "/v1/credits", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CreditCheckResponse, + ) + + +class CreditsResourceWithRawResponse: + def __init__(self, credits: CreditsResource) -> None: + self._credits = credits + + self.check = to_raw_response_wrapper( + credits.check, + ) + + +class AsyncCreditsResourceWithRawResponse: + def __init__(self, credits: AsyncCreditsResource) -> None: + self._credits = credits + + self.check = async_to_raw_response_wrapper( + credits.check, + ) + + +class CreditsResourceWithStreamingResponse: + def __init__(self, credits: CreditsResource) -> None: + self._credits = credits + + self.check = to_streamed_response_wrapper( + credits.check, + ) + + +class AsyncCreditsResourceWithStreamingResponse: + def __init__(self, credits: AsyncCreditsResource) -> None: + self._credits = credits + + self.check = async_to_streamed_response_wrapper( + credits.check, + ) diff --git a/src/cas_parser/resources/inbound_email.py b/src/cas_parser/resources/inbound_email.py new file mode 100644 index 0000000..e20ae7b --- /dev/null +++ b/src/cas_parser/resources/inbound_email.py @@ -0,0 +1,563 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, List +from typing_extensions import Literal + +import httpx + +from ..types import inbound_email_list_params, inbound_email_create_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.inbound_email_list_response import InboundEmailListResponse +from ..types.inbound_email_create_response import InboundEmailCreateResponse +from ..types.inbound_email_delete_response import InboundEmailDeleteResponse +from ..types.inbound_email_retrieve_response import InboundEmailRetrieveResponse + +__all__ = ["InboundEmailResource", "AsyncInboundEmailResource"] + + +class InboundEmailResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> InboundEmailResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return InboundEmailResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> InboundEmailResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response + """ + return InboundEmailResourceWithStreamingResponse(self) + + def create( + self, + *, + callback_url: str, + alias: str | Omit = omit, + allowed_sources: List[Literal["cdsl", "nsdl", "cams", "kfintech"]] | Omit = omit, + metadata: Dict[str, str] | Omit = omit, + reference: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboundEmailCreateResponse: + """ + Create a dedicated inbound email address for collecting CAS statements via email + forwarding. + + **How it works:** + + 1. Create an inbound email with your webhook URL + 2. Display the email address to your user (e.g., "Forward your CAS to + ie_xxx@import.casparser.in") + 3. When an investor forwards a CAS email, we verify the sender and deliver to + your webhook + + **Webhook Delivery:** + + - We POST to your `callback_url` with JSON body containing files (matching + EmailCASFile schema) + - Failed deliveries are retried automatically with exponential backoff + + **Inactivity:** + + - Inbound emails with no activity in 30 days are marked inactive + - Active inbound emails remain operational indefinitely + + Args: + callback_url: Webhook URL where we POST email notifications. Must be HTTPS in production (HTTP + allowed for localhost during development). + + alias: Optional custom email prefix for user-friendly addresses. + + - Must be 3-32 characters + - Alphanumeric + hyphens only + - Must start and end with letter/number + - Example: `john-portfolio@import.casparser.in` + - If omitted, generates random ID like `ie_abc123xyz@import.casparser.in` + + allowed_sources: Filter emails by CAS provider. If omitted, accepts all providers. + + - `cdsl` → eCAS@cdslstatement.com + - `nsdl` → NSDL-CAS@nsdl.co.in + - `cams` → donotreply@camsonline.com + - `kfintech` → samfS@kfintech.com + + metadata: Optional key-value pairs (max 10) to include in webhook payload. Useful for + passing context like plan_type, campaign_id, etc. + + reference: Your internal identifier (e.g., user_id, account_id). Returned in webhook + payload for correlation. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v4/inbound-email", + body=maybe_transform( + { + "callback_url": callback_url, + "alias": alias, + "allowed_sources": allowed_sources, + "metadata": metadata, + "reference": reference, + }, + inbound_email_create_params.InboundEmailCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboundEmailCreateResponse, + ) + + def retrieve( + self, + inbound_email_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboundEmailRetrieveResponse: + """ + Retrieve details of a specific mailbox including statistics. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not inbound_email_id: + raise ValueError(f"Expected a non-empty value for `inbound_email_id` but received {inbound_email_id!r}") + return self._get( + f"/v4/inbound-email/{inbound_email_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboundEmailRetrieveResponse, + ) + + def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + status: Literal["active", "paused", "all"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboundEmailListResponse: + """List all mailboxes associated with your API key. + + Returns active and inactive + mailboxes (deleted mailboxes are excluded). + + Args: + limit: Maximum number of inbound emails to return + + offset: Pagination offset + + status: Filter by status + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/v4/inbound-email", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "status": status, + }, + inbound_email_list_params.InboundEmailListParams, + ), + ), + cast_to=InboundEmailListResponse, + ) + + def delete( + self, + inbound_email_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboundEmailDeleteResponse: + """Permanently delete an inbound email address. + + It will stop accepting emails. + + **Note:** Deletion is immediate and cannot be undone. Any emails received after + deletion will be rejected. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not inbound_email_id: + raise ValueError(f"Expected a non-empty value for `inbound_email_id` but received {inbound_email_id!r}") + return self._delete( + f"/v4/inbound-email/{inbound_email_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboundEmailDeleteResponse, + ) + + +class AsyncInboundEmailResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncInboundEmailResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncInboundEmailResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncInboundEmailResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response + """ + return AsyncInboundEmailResourceWithStreamingResponse(self) + + async def create( + self, + *, + callback_url: str, + alias: str | Omit = omit, + allowed_sources: List[Literal["cdsl", "nsdl", "cams", "kfintech"]] | Omit = omit, + metadata: Dict[str, str] | Omit = omit, + reference: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboundEmailCreateResponse: + """ + Create a dedicated inbound email address for collecting CAS statements via email + forwarding. + + **How it works:** + + 1. Create an inbound email with your webhook URL + 2. Display the email address to your user (e.g., "Forward your CAS to + ie_xxx@import.casparser.in") + 3. When an investor forwards a CAS email, we verify the sender and deliver to + your webhook + + **Webhook Delivery:** + + - We POST to your `callback_url` with JSON body containing files (matching + EmailCASFile schema) + - Failed deliveries are retried automatically with exponential backoff + + **Inactivity:** + + - Inbound emails with no activity in 30 days are marked inactive + - Active inbound emails remain operational indefinitely + + Args: + callback_url: Webhook URL where we POST email notifications. Must be HTTPS in production (HTTP + allowed for localhost during development). + + alias: Optional custom email prefix for user-friendly addresses. + + - Must be 3-32 characters + - Alphanumeric + hyphens only + - Must start and end with letter/number + - Example: `john-portfolio@import.casparser.in` + - If omitted, generates random ID like `ie_abc123xyz@import.casparser.in` + + allowed_sources: Filter emails by CAS provider. If omitted, accepts all providers. + + - `cdsl` → eCAS@cdslstatement.com + - `nsdl` → NSDL-CAS@nsdl.co.in + - `cams` → donotreply@camsonline.com + - `kfintech` → samfS@kfintech.com + + metadata: Optional key-value pairs (max 10) to include in webhook payload. Useful for + passing context like plan_type, campaign_id, etc. + + reference: Your internal identifier (e.g., user_id, account_id). Returned in webhook + payload for correlation. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v4/inbound-email", + body=await async_maybe_transform( + { + "callback_url": callback_url, + "alias": alias, + "allowed_sources": allowed_sources, + "metadata": metadata, + "reference": reference, + }, + inbound_email_create_params.InboundEmailCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboundEmailCreateResponse, + ) + + async def retrieve( + self, + inbound_email_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboundEmailRetrieveResponse: + """ + Retrieve details of a specific mailbox including statistics. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not inbound_email_id: + raise ValueError(f"Expected a non-empty value for `inbound_email_id` but received {inbound_email_id!r}") + return await self._get( + f"/v4/inbound-email/{inbound_email_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboundEmailRetrieveResponse, + ) + + async def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + status: Literal["active", "paused", "all"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboundEmailListResponse: + """List all mailboxes associated with your API key. + + Returns active and inactive + mailboxes (deleted mailboxes are excluded). + + Args: + limit: Maximum number of inbound emails to return + + offset: Pagination offset + + status: Filter by status + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/v4/inbound-email", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + "status": status, + }, + inbound_email_list_params.InboundEmailListParams, + ), + ), + cast_to=InboundEmailListResponse, + ) + + async def delete( + self, + inbound_email_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> InboundEmailDeleteResponse: + """Permanently delete an inbound email address. + + It will stop accepting emails. + + **Note:** Deletion is immediate and cannot be undone. Any emails received after + deletion will be rejected. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not inbound_email_id: + raise ValueError(f"Expected a non-empty value for `inbound_email_id` but received {inbound_email_id!r}") + return await self._delete( + f"/v4/inbound-email/{inbound_email_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=InboundEmailDeleteResponse, + ) + + +class InboundEmailResourceWithRawResponse: + def __init__(self, inbound_email: InboundEmailResource) -> None: + self._inbound_email = inbound_email + + self.create = to_raw_response_wrapper( + inbound_email.create, + ) + self.retrieve = to_raw_response_wrapper( + inbound_email.retrieve, + ) + self.list = to_raw_response_wrapper( + inbound_email.list, + ) + self.delete = to_raw_response_wrapper( + inbound_email.delete, + ) + + +class AsyncInboundEmailResourceWithRawResponse: + def __init__(self, inbound_email: AsyncInboundEmailResource) -> None: + self._inbound_email = inbound_email + + self.create = async_to_raw_response_wrapper( + inbound_email.create, + ) + self.retrieve = async_to_raw_response_wrapper( + inbound_email.retrieve, + ) + self.list = async_to_raw_response_wrapper( + inbound_email.list, + ) + self.delete = async_to_raw_response_wrapper( + inbound_email.delete, + ) + + +class InboundEmailResourceWithStreamingResponse: + def __init__(self, inbound_email: InboundEmailResource) -> None: + self._inbound_email = inbound_email + + self.create = to_streamed_response_wrapper( + inbound_email.create, + ) + self.retrieve = to_streamed_response_wrapper( + inbound_email.retrieve, + ) + self.list = to_streamed_response_wrapper( + inbound_email.list, + ) + self.delete = to_streamed_response_wrapper( + inbound_email.delete, + ) + + +class AsyncInboundEmailResourceWithStreamingResponse: + def __init__(self, inbound_email: AsyncInboundEmailResource) -> None: + self._inbound_email = inbound_email + + self.create = async_to_streamed_response_wrapper( + inbound_email.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + inbound_email.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + inbound_email.list, + ) + self.delete = async_to_streamed_response_wrapper( + inbound_email.delete, + ) diff --git a/src/cas_parser/resources/logs.py b/src/cas_parser/resources/logs.py new file mode 100644 index 0000000..45ba056 --- /dev/null +++ b/src/cas_parser/resources/logs.py @@ -0,0 +1,315 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime + +import httpx + +from ..types import log_create_params, log_get_summary_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.log_create_response import LogCreateResponse +from ..types.log_get_summary_response import LogGetSummaryResponse + +__all__ = ["LogsResource", "AsyncLogsResource"] + + +class LogsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> LogsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return LogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response + """ + return LogsResourceWithStreamingResponse(self) + + def create( + self, + *, + end_time: Union[str, datetime] | Omit = omit, + limit: int | Omit = omit, + start_time: Union[str, datetime] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogCreateResponse: + """ + Retrieve detailed API usage logs for your account. + + Returns a list of API calls with timestamps, features used, status codes, and + credits consumed. Useful for monitoring usage patterns and debugging. + + **Legacy path:** `/logs` (still supported) + + Args: + end_time: End time filter (ISO 8601). Defaults to now. + + limit: Maximum number of logs to return + + start_time: Start time filter (ISO 8601). Defaults to 30 days ago. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/usage", + body=maybe_transform( + { + "end_time": end_time, + "limit": limit, + "start_time": start_time, + }, + log_create_params.LogCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogCreateResponse, + ) + + def get_summary( + self, + *, + end_time: Union[str, datetime] | Omit = omit, + start_time: Union[str, datetime] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogGetSummaryResponse: + """ + Get aggregated usage statistics grouped by feature. + + Useful for understanding which API features are being used most and tracking + usage trends. + + **Legacy path:** `/logs/summary` (still supported) + + Args: + end_time: End time filter (ISO 8601). Defaults to now. + + start_time: Start time filter (ISO 8601). Defaults to start of current month. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/usage/summary", + body=maybe_transform( + { + "end_time": end_time, + "start_time": start_time, + }, + log_get_summary_params.LogGetSummaryParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogGetSummaryResponse, + ) + + +class AsyncLogsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncLogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response + """ + return AsyncLogsResourceWithStreamingResponse(self) + + async def create( + self, + *, + end_time: Union[str, datetime] | Omit = omit, + limit: int | Omit = omit, + start_time: Union[str, datetime] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogCreateResponse: + """ + Retrieve detailed API usage logs for your account. + + Returns a list of API calls with timestamps, features used, status codes, and + credits consumed. Useful for monitoring usage patterns and debugging. + + **Legacy path:** `/logs` (still supported) + + Args: + end_time: End time filter (ISO 8601). Defaults to now. + + limit: Maximum number of logs to return + + start_time: Start time filter (ISO 8601). Defaults to 30 days ago. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/usage", + body=await async_maybe_transform( + { + "end_time": end_time, + "limit": limit, + "start_time": start_time, + }, + log_create_params.LogCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogCreateResponse, + ) + + async def get_summary( + self, + *, + end_time: Union[str, datetime] | Omit = omit, + start_time: Union[str, datetime] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogGetSummaryResponse: + """ + Get aggregated usage statistics grouped by feature. + + Useful for understanding which API features are being used most and tracking + usage trends. + + **Legacy path:** `/logs/summary` (still supported) + + Args: + end_time: End time filter (ISO 8601). Defaults to now. + + start_time: Start time filter (ISO 8601). Defaults to start of current month. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/usage/summary", + body=await async_maybe_transform( + { + "end_time": end_time, + "start_time": start_time, + }, + log_get_summary_params.LogGetSummaryParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogGetSummaryResponse, + ) + + +class LogsResourceWithRawResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.create = to_raw_response_wrapper( + logs.create, + ) + self.get_summary = to_raw_response_wrapper( + logs.get_summary, + ) + + +class AsyncLogsResourceWithRawResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.create = async_to_raw_response_wrapper( + logs.create, + ) + self.get_summary = async_to_raw_response_wrapper( + logs.get_summary, + ) + + +class LogsResourceWithStreamingResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.create = to_streamed_response_wrapper( + logs.create, + ) + self.get_summary = to_streamed_response_wrapper( + logs.get_summary, + ) + + +class AsyncLogsResourceWithStreamingResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.create = async_to_streamed_response_wrapper( + logs.create, + ) + self.get_summary = async_to_streamed_response_wrapper( + logs.get_summary, + ) diff --git a/src/cas_parser/resources/verify_token.py b/src/cas_parser/resources/verify_token.py new file mode 100644 index 0000000..06981d4 --- /dev/null +++ b/src/cas_parser/resources/verify_token.py @@ -0,0 +1,143 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .._types import Body, Query, Headers, NotGiven, not_given +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.verify_token_verify_response import VerifyTokenVerifyResponse + +__all__ = ["VerifyTokenResource", "AsyncVerifyTokenResource"] + + +class VerifyTokenResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> VerifyTokenResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return VerifyTokenResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> VerifyTokenResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response + """ + return VerifyTokenResourceWithStreamingResponse(self) + + def verify( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> VerifyTokenVerifyResponse: + """Verify an access token and check if it's still valid. + + Useful for debugging token + issues. + """ + return self._post( + "/v1/token/verify", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=VerifyTokenVerifyResponse, + ) + + +class AsyncVerifyTokenResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncVerifyTokenResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/CASParser/cas-parser-python#accessing-raw-response-data-eg-headers + """ + return AsyncVerifyTokenResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncVerifyTokenResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/CASParser/cas-parser-python#with_streaming_response + """ + return AsyncVerifyTokenResourceWithStreamingResponse(self) + + async def verify( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> VerifyTokenVerifyResponse: + """Verify an access token and check if it's still valid. + + Useful for debugging token + issues. + """ + return await self._post( + "/v1/token/verify", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=VerifyTokenVerifyResponse, + ) + + +class VerifyTokenResourceWithRawResponse: + def __init__(self, verify_token: VerifyTokenResource) -> None: + self._verify_token = verify_token + + self.verify = to_raw_response_wrapper( + verify_token.verify, + ) + + +class AsyncVerifyTokenResourceWithRawResponse: + def __init__(self, verify_token: AsyncVerifyTokenResource) -> None: + self._verify_token = verify_token + + self.verify = async_to_raw_response_wrapper( + verify_token.verify, + ) + + +class VerifyTokenResourceWithStreamingResponse: + def __init__(self, verify_token: VerifyTokenResource) -> None: + self._verify_token = verify_token + + self.verify = to_streamed_response_wrapper( + verify_token.verify, + ) + + +class AsyncVerifyTokenResourceWithStreamingResponse: + def __init__(self, verify_token: AsyncVerifyTokenResource) -> None: + self._verify_token = verify_token + + self.verify = async_to_streamed_response_wrapper( + verify_token.verify, + ) diff --git a/src/cas_parser/types/__init__.py b/src/cas_parser/types/__init__.py index ee5666d..269b048 100644 --- a/src/cas_parser/types/__init__.py +++ b/src/cas_parser/types/__init__.py @@ -5,18 +5,32 @@ from .transaction import Transaction as Transaction from .linked_holder import LinkedHolder as LinkedHolder from .unified_response import UnifiedResponse as UnifiedResponse +from .log_create_params import LogCreateParams as LogCreateParams from .nsdl_parse_params import NsdlParseParams as NsdlParseParams +from .log_create_response import LogCreateResponse as LogCreateResponse from .cdsl_parse_pdf_params import CdslParsePdfParams as CdslParsePdfParams +from .credit_check_response import CreditCheckResponse as CreditCheckResponse +from .log_get_summary_params import LogGetSummaryParams as LogGetSummaryParams +from .log_get_summary_response import LogGetSummaryResponse as LogGetSummaryResponse +from .inbound_email_list_params import InboundEmailListParams as InboundEmailListParams +from .access_token_create_params import AccessTokenCreateParams as AccessTokenCreateParams from .cams_kfintech_parse_params import CamsKfintechParseParams as CamsKfintechParseParams from .contract_note_parse_params import ContractNoteParseParams as ContractNoteParseParams from .inbox_connect_email_params import InboxConnectEmailParams as InboxConnectEmailParams from .smart_parse_cas_pdf_params import SmartParseCasPdfParams as SmartParseCasPdfParams +from .inbound_email_create_params import InboundEmailCreateParams as InboundEmailCreateParams +from .inbound_email_list_response import InboundEmailListResponse as InboundEmailListResponse from .inbox_list_cas_files_params import InboxListCasFilesParams as InboxListCasFilesParams +from .access_token_create_response import AccessTokenCreateResponse as AccessTokenCreateResponse from .contract_note_parse_response import ContractNoteParseResponse as ContractNoteParseResponse from .inbox_connect_email_response import InboxConnectEmailResponse as InboxConnectEmailResponse from .kfintech_generate_cas_params import KfintechGenerateCasParams as KfintechGenerateCasParams +from .verify_token_verify_response import VerifyTokenVerifyResponse as VerifyTokenVerifyResponse +from .inbound_email_create_response import InboundEmailCreateResponse as InboundEmailCreateResponse +from .inbound_email_delete_response import InboundEmailDeleteResponse as InboundEmailDeleteResponse from .inbox_list_cas_files_response import InboxListCasFilesResponse as InboxListCasFilesResponse from .kfintech_generate_cas_response import KfintechGenerateCasResponse as KfintechGenerateCasResponse +from .inbound_email_retrieve_response import InboundEmailRetrieveResponse as InboundEmailRetrieveResponse from .inbox_disconnect_email_response import InboxDisconnectEmailResponse as InboxDisconnectEmailResponse from .inbox_check_connection_status_response import ( InboxCheckConnectionStatusResponse as InboxCheckConnectionStatusResponse, diff --git a/src/cas_parser/types/access_token_create_params.py b/src/cas_parser/types/access_token_create_params.py new file mode 100644 index 0000000..1d3f392 --- /dev/null +++ b/src/cas_parser/types/access_token_create_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["AccessTokenCreateParams"] + + +class AccessTokenCreateParams(TypedDict, total=False): + expiry_minutes: int + """Token validity in minutes (max 60)""" diff --git a/src/cas_parser/types/access_token_create_response.py b/src/cas_parser/types/access_token_create_response.py new file mode 100644 index 0000000..9a03c8c --- /dev/null +++ b/src/cas_parser/types/access_token_create_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["AccessTokenCreateResponse"] + + +class AccessTokenCreateResponse(BaseModel): + access_token: Optional[str] = None + """The at\\__ prefixed access token""" + + expires_in: Optional[int] = None + """Token validity in seconds""" + + token_type: Optional[str] = None + """Always "api_key" - token is a drop-in replacement for x-api-key header""" diff --git a/src/cas_parser/types/credit_check_response.py b/src/cas_parser/types/credit_check_response.py new file mode 100644 index 0000000..9396b9f --- /dev/null +++ b/src/cas_parser/types/credit_check_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["CreditCheckResponse"] + + +class CreditCheckResponse(BaseModel): + enabled_features: Optional[List[str]] = None + """List of API features enabled for your plan""" + + is_unlimited: Optional[bool] = None + """Whether the account has unlimited credits""" + + limit: Optional[int] = None + """Total credit limit for billing period""" + + remaining: Optional[float] = None + """Remaining credits (null if unlimited)""" + + resets_at: Optional[datetime] = None + """When credits reset (ISO 8601)""" + + used: Optional[float] = None + """Number of credits used this billing period""" diff --git a/src/cas_parser/types/inbound_email_create_params.py b/src/cas_parser/types/inbound_email_create_params.py new file mode 100644 index 0000000..356d7e1 --- /dev/null +++ b/src/cas_parser/types/inbound_email_create_params.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, List +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["InboundEmailCreateParams"] + + +class InboundEmailCreateParams(TypedDict, total=False): + callback_url: Required[str] + """ + Webhook URL where we POST email notifications. Must be HTTPS in production (HTTP + allowed for localhost during development). + """ + + alias: str + """Optional custom email prefix for user-friendly addresses. + + - Must be 3-32 characters + - Alphanumeric + hyphens only + - Must start and end with letter/number + - Example: `john-portfolio@import.casparser.in` + - If omitted, generates random ID like `ie_abc123xyz@import.casparser.in` + """ + + allowed_sources: List[Literal["cdsl", "nsdl", "cams", "kfintech"]] + """Filter emails by CAS provider. If omitted, accepts all providers. + + - `cdsl` → eCAS@cdslstatement.com + - `nsdl` → NSDL-CAS@nsdl.co.in + - `cams` → donotreply@camsonline.com + - `kfintech` → samfS@kfintech.com + """ + + metadata: Dict[str, str] + """ + Optional key-value pairs (max 10) to include in webhook payload. Useful for + passing context like plan_type, campaign_id, etc. + """ + + reference: str + """ + Your internal identifier (e.g., user_id, account_id). Returned in webhook + payload for correlation. + """ diff --git a/src/cas_parser/types/inbound_email_create_response.py b/src/cas_parser/types/inbound_email_create_response.py new file mode 100644 index 0000000..29f89dc --- /dev/null +++ b/src/cas_parser/types/inbound_email_create_response.py @@ -0,0 +1,40 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["InboundEmailCreateResponse"] + + +class InboundEmailCreateResponse(BaseModel): + """An inbound email address for receiving forwarded CAS emails""" + + allowed_sources: Optional[List[Literal["cdsl", "nsdl", "cams", "kfintech"]]] = None + """Accepted CAS providers (empty = all)""" + + callback_url: Optional[str] = None + """Webhook URL for email notifications""" + + created_at: Optional[datetime] = None + """When the mailbox was created""" + + email: Optional[str] = None + """The inbound email address to forward CAS statements to""" + + inbound_email_id: Optional[str] = None + """Unique inbound email identifier""" + + metadata: Optional[Dict[str, str]] = None + """Custom key-value metadata""" + + reference: Optional[str] = None + """Your internal reference identifier""" + + status: Optional[Literal["active", "paused"]] = None + """Current mailbox status""" + + updated_at: Optional[datetime] = None + """When the mailbox was last updated""" diff --git a/src/cas_parser/types/inbound_email_delete_response.py b/src/cas_parser/types/inbound_email_delete_response.py new file mode 100644 index 0000000..fdb55b2 --- /dev/null +++ b/src/cas_parser/types/inbound_email_delete_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["InboundEmailDeleteResponse"] + + +class InboundEmailDeleteResponse(BaseModel): + msg: Optional[str] = None + + status: Optional[str] = None diff --git a/src/cas_parser/types/inbound_email_list_params.py b/src/cas_parser/types/inbound_email_list_params.py new file mode 100644 index 0000000..c70d034 --- /dev/null +++ b/src/cas_parser/types/inbound_email_list_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["InboundEmailListParams"] + + +class InboundEmailListParams(TypedDict, total=False): + limit: int + """Maximum number of inbound emails to return""" + + offset: int + """Pagination offset""" + + status: Literal["active", "paused", "all"] + """Filter by status""" diff --git a/src/cas_parser/types/inbound_email_list_response.py b/src/cas_parser/types/inbound_email_list_response.py new file mode 100644 index 0000000..b1eea1b --- /dev/null +++ b/src/cas_parser/types/inbound_email_list_response.py @@ -0,0 +1,53 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["InboundEmailListResponse", "InboundEmail"] + + +class InboundEmail(BaseModel): + """An inbound email address for receiving forwarded CAS emails""" + + allowed_sources: Optional[List[Literal["cdsl", "nsdl", "cams", "kfintech"]]] = None + """Accepted CAS providers (empty = all)""" + + callback_url: Optional[str] = None + """Webhook URL for email notifications""" + + created_at: Optional[datetime] = None + """When the mailbox was created""" + + email: Optional[str] = None + """The inbound email address to forward CAS statements to""" + + inbound_email_id: Optional[str] = None + """Unique inbound email identifier""" + + metadata: Optional[Dict[str, str]] = None + """Custom key-value metadata""" + + reference: Optional[str] = None + """Your internal reference identifier""" + + status: Optional[Literal["active", "paused"]] = None + """Current mailbox status""" + + updated_at: Optional[datetime] = None + """When the mailbox was last updated""" + + +class InboundEmailListResponse(BaseModel): + inbound_emails: Optional[List[InboundEmail]] = None + + limit: Optional[int] = None + + offset: Optional[int] = None + + status: Optional[str] = None + + total: Optional[int] = None + """Total number of inbound emails (for pagination)""" diff --git a/src/cas_parser/types/inbound_email_retrieve_response.py b/src/cas_parser/types/inbound_email_retrieve_response.py new file mode 100644 index 0000000..601fc87 --- /dev/null +++ b/src/cas_parser/types/inbound_email_retrieve_response.py @@ -0,0 +1,40 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, List, Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["InboundEmailRetrieveResponse"] + + +class InboundEmailRetrieveResponse(BaseModel): + """An inbound email address for receiving forwarded CAS emails""" + + allowed_sources: Optional[List[Literal["cdsl", "nsdl", "cams", "kfintech"]]] = None + """Accepted CAS providers (empty = all)""" + + callback_url: Optional[str] = None + """Webhook URL for email notifications""" + + created_at: Optional[datetime] = None + """When the mailbox was created""" + + email: Optional[str] = None + """The inbound email address to forward CAS statements to""" + + inbound_email_id: Optional[str] = None + """Unique inbound email identifier""" + + metadata: Optional[Dict[str, str]] = None + """Custom key-value metadata""" + + reference: Optional[str] = None + """Your internal reference identifier""" + + status: Optional[Literal["active", "paused"]] = None + """Current mailbox status""" + + updated_at: Optional[datetime] = None + """When the mailbox was last updated""" diff --git a/src/cas_parser/types/log_create_params.py b/src/cas_parser/types/log_create_params.py new file mode 100644 index 0000000..6104297 --- /dev/null +++ b/src/cas_parser/types/log_create_params.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["LogCreateParams"] + + +class LogCreateParams(TypedDict, total=False): + end_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """End time filter (ISO 8601). Defaults to now.""" + + limit: int + """Maximum number of logs to return""" + + start_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """Start time filter (ISO 8601). Defaults to 30 days ago.""" diff --git a/src/cas_parser/types/log_create_response.py b/src/cas_parser/types/log_create_response.py new file mode 100644 index 0000000..446d6e5 --- /dev/null +++ b/src/cas_parser/types/log_create_response.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["LogCreateResponse", "Log"] + + +class Log(BaseModel): + credits: Optional[float] = None + """Credits consumed for this request""" + + feature: Optional[str] = None + """API feature used""" + + path: Optional[str] = None + """API endpoint path""" + + request_id: Optional[str] = None + """Unique request identifier""" + + status_code: Optional[int] = None + """HTTP response status code""" + + timestamp: Optional[datetime] = None + """When the request was made""" + + +class LogCreateResponse(BaseModel): + count: Optional[int] = None + """Number of logs returned""" + + logs: Optional[List[Log]] = None + + status: Optional[str] = None diff --git a/src/cas_parser/types/log_get_summary_params.py b/src/cas_parser/types/log_get_summary_params.py new file mode 100644 index 0000000..fc9ffe7 --- /dev/null +++ b/src/cas_parser/types/log_get_summary_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["LogGetSummaryParams"] + + +class LogGetSummaryParams(TypedDict, total=False): + end_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """End time filter (ISO 8601). Defaults to now.""" + + start_time: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """Start time filter (ISO 8601). Defaults to start of current month.""" diff --git a/src/cas_parser/types/log_get_summary_response.py b/src/cas_parser/types/log_get_summary_response.py new file mode 100644 index 0000000..d947f84 --- /dev/null +++ b/src/cas_parser/types/log_get_summary_response.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["LogGetSummaryResponse", "Summary", "SummaryByFeature"] + + +class SummaryByFeature(BaseModel): + credits: Optional[float] = None + """Credits consumed by this feature""" + + feature: Optional[str] = None + """API feature name""" + + requests: Optional[int] = None + """Number of requests for this feature""" + + +class Summary(BaseModel): + by_feature: Optional[List[SummaryByFeature]] = None + """Usage breakdown by feature""" + + total_credits: Optional[float] = None + """Total credits consumed in the period""" + + total_requests: Optional[int] = None + """Total API requests made in the period""" + + +class LogGetSummaryResponse(BaseModel): + status: Optional[str] = None + + summary: Optional[Summary] = None diff --git a/src/cas_parser/types/verify_token_verify_response.py b/src/cas_parser/types/verify_token_verify_response.py new file mode 100644 index 0000000..fcfebe7 --- /dev/null +++ b/src/cas_parser/types/verify_token_verify_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["VerifyTokenVerifyResponse"] + + +class VerifyTokenVerifyResponse(BaseModel): + error: Optional[str] = None + """Error message (only shown if invalid)""" + + masked_api_key: Optional[str] = None + """Masked API key (only shown if valid)""" + + valid: Optional[bool] = None + """Whether the token is valid""" diff --git a/tests/api_resources/test_access_token.py b/tests/api_resources/test_access_token.py new file mode 100644 index 0000000..32d63c9 --- /dev/null +++ b/tests/api_resources/test_access_token.py @@ -0,0 +1,96 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import AccessTokenCreateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAccessToken: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: CasParser) -> None: + access_token = client.access_token.create() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: CasParser) -> None: + access_token = client.access_token.create( + expiry_minutes=60, + ) + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: CasParser) -> None: + response = client.access_token.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + access_token = response.parse() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: CasParser) -> None: + with client.access_token.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + access_token = response.parse() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncAccessToken: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncCasParser) -> None: + access_token = await async_client.access_token.create() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncCasParser) -> None: + access_token = await async_client.access_token.create( + expiry_minutes=60, + ) + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: + response = await async_client.access_token.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + access_token = await response.parse() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncCasParser) -> None: + async with async_client.access_token.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + access_token = await response.parse() + assert_matches_type(AccessTokenCreateResponse, access_token, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_credits.py b/tests/api_resources/test_credits.py new file mode 100644 index 0000000..8538889 --- /dev/null +++ b/tests/api_resources/test_credits.py @@ -0,0 +1,80 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import CreditCheckResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCredits: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_check(self, client: CasParser) -> None: + credit = client.credits.check() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_check(self, client: CasParser) -> None: + response = client.credits.with_raw_response.check() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credit = response.parse() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_check(self, client: CasParser) -> None: + with client.credits.with_streaming_response.check() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credit = response.parse() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncCredits: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_check(self, async_client: AsyncCasParser) -> None: + credit = await async_client.credits.check() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_check(self, async_client: AsyncCasParser) -> None: + response = await async_client.credits.with_raw_response.check() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + credit = await response.parse() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_check(self, async_client: AsyncCasParser) -> None: + async with async_client.credits.with_streaming_response.check() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + credit = await response.parse() + assert_matches_type(CreditCheckResponse, credit, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_inbound_email.py b/tests/api_resources/test_inbound_email.py new file mode 100644 index 0000000..6970229 --- /dev/null +++ b/tests/api_resources/test_inbound_email.py @@ -0,0 +1,371 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import ( + InboundEmailListResponse, + InboundEmailCreateResponse, + InboundEmailDeleteResponse, + InboundEmailRetrieveResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestInboundEmail: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: CasParser) -> None: + inbound_email = client.inbound_email.create( + callback_url="https://api.yourapp.com/webhooks/cas-email", + ) + assert_matches_type(InboundEmailCreateResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: CasParser) -> None: + inbound_email = client.inbound_email.create( + callback_url="https://api.yourapp.com/webhooks/cas-email", + alias="john-portfolio", + allowed_sources=["cdsl", "nsdl"], + metadata={ + "plan": "premium", + "source": "onboarding", + }, + reference="user_12345", + ) + assert_matches_type(InboundEmailCreateResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: CasParser) -> None: + response = client.inbound_email.with_raw_response.create( + callback_url="https://api.yourapp.com/webhooks/cas-email", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbound_email = response.parse() + assert_matches_type(InboundEmailCreateResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: CasParser) -> None: + with client.inbound_email.with_streaming_response.create( + callback_url="https://api.yourapp.com/webhooks/cas-email", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbound_email = response.parse() + assert_matches_type(InboundEmailCreateResponse, inbound_email, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: CasParser) -> None: + inbound_email = client.inbound_email.retrieve( + "ie_a1b2c3d4e5f6", + ) + assert_matches_type(InboundEmailRetrieveResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: CasParser) -> None: + response = client.inbound_email.with_raw_response.retrieve( + "ie_a1b2c3d4e5f6", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbound_email = response.parse() + assert_matches_type(InboundEmailRetrieveResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: CasParser) -> None: + with client.inbound_email.with_streaming_response.retrieve( + "ie_a1b2c3d4e5f6", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbound_email = response.parse() + assert_matches_type(InboundEmailRetrieveResponse, inbound_email, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: CasParser) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `inbound_email_id` but received ''"): + client.inbound_email.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: CasParser) -> None: + inbound_email = client.inbound_email.list() + assert_matches_type(InboundEmailListResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: CasParser) -> None: + inbound_email = client.inbound_email.list( + limit=1, + offset=0, + status="active", + ) + assert_matches_type(InboundEmailListResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: CasParser) -> None: + response = client.inbound_email.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbound_email = response.parse() + assert_matches_type(InboundEmailListResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: CasParser) -> None: + with client.inbound_email.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbound_email = response.parse() + assert_matches_type(InboundEmailListResponse, inbound_email, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_delete(self, client: CasParser) -> None: + inbound_email = client.inbound_email.delete( + "inbound_email_id", + ) + assert_matches_type(InboundEmailDeleteResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: CasParser) -> None: + response = client.inbound_email.with_raw_response.delete( + "inbound_email_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbound_email = response.parse() + assert_matches_type(InboundEmailDeleteResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: CasParser) -> None: + with client.inbound_email.with_streaming_response.delete( + "inbound_email_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbound_email = response.parse() + assert_matches_type(InboundEmailDeleteResponse, inbound_email, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_delete(self, client: CasParser) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `inbound_email_id` but received ''"): + client.inbound_email.with_raw_response.delete( + "", + ) + + +class TestAsyncInboundEmail: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncCasParser) -> None: + inbound_email = await async_client.inbound_email.create( + callback_url="https://api.yourapp.com/webhooks/cas-email", + ) + assert_matches_type(InboundEmailCreateResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncCasParser) -> None: + inbound_email = await async_client.inbound_email.create( + callback_url="https://api.yourapp.com/webhooks/cas-email", + alias="john-portfolio", + allowed_sources=["cdsl", "nsdl"], + metadata={ + "plan": "premium", + "source": "onboarding", + }, + reference="user_12345", + ) + assert_matches_type(InboundEmailCreateResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: + response = await async_client.inbound_email.with_raw_response.create( + callback_url="https://api.yourapp.com/webhooks/cas-email", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbound_email = await response.parse() + assert_matches_type(InboundEmailCreateResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncCasParser) -> None: + async with async_client.inbound_email.with_streaming_response.create( + callback_url="https://api.yourapp.com/webhooks/cas-email", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbound_email = await response.parse() + assert_matches_type(InboundEmailCreateResponse, inbound_email, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncCasParser) -> None: + inbound_email = await async_client.inbound_email.retrieve( + "ie_a1b2c3d4e5f6", + ) + assert_matches_type(InboundEmailRetrieveResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncCasParser) -> None: + response = await async_client.inbound_email.with_raw_response.retrieve( + "ie_a1b2c3d4e5f6", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbound_email = await response.parse() + assert_matches_type(InboundEmailRetrieveResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncCasParser) -> None: + async with async_client.inbound_email.with_streaming_response.retrieve( + "ie_a1b2c3d4e5f6", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbound_email = await response.parse() + assert_matches_type(InboundEmailRetrieveResponse, inbound_email, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncCasParser) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `inbound_email_id` but received ''"): + await async_client.inbound_email.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncCasParser) -> None: + inbound_email = await async_client.inbound_email.list() + assert_matches_type(InboundEmailListResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncCasParser) -> None: + inbound_email = await async_client.inbound_email.list( + limit=1, + offset=0, + status="active", + ) + assert_matches_type(InboundEmailListResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncCasParser) -> None: + response = await async_client.inbound_email.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbound_email = await response.parse() + assert_matches_type(InboundEmailListResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncCasParser) -> None: + async with async_client.inbound_email.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbound_email = await response.parse() + assert_matches_type(InboundEmailListResponse, inbound_email, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncCasParser) -> None: + inbound_email = await async_client.inbound_email.delete( + "inbound_email_id", + ) + assert_matches_type(InboundEmailDeleteResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncCasParser) -> None: + response = await async_client.inbound_email.with_raw_response.delete( + "inbound_email_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + inbound_email = await response.parse() + assert_matches_type(InboundEmailDeleteResponse, inbound_email, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncCasParser) -> None: + async with async_client.inbound_email.with_streaming_response.delete( + "inbound_email_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + inbound_email = await response.parse() + assert_matches_type(InboundEmailDeleteResponse, inbound_email, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncCasParser) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `inbound_email_id` but received ''"): + await async_client.inbound_email.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_logs.py b/tests/api_resources/test_logs.py new file mode 100644 index 0000000..43908ef --- /dev/null +++ b/tests/api_resources/test_logs.py @@ -0,0 +1,175 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import LogCreateResponse, LogGetSummaryResponse +from cas_parser._utils import parse_datetime + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestLogs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: CasParser) -> None: + log = client.logs.create() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: CasParser) -> None: + log = client.logs.create( + end_time=parse_datetime("2026-01-31T23:59:59Z"), + limit=1, + start_time=parse_datetime("2026-01-01T00:00:00Z"), + ) + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: CasParser) -> None: + response = client.logs.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = response.parse() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: CasParser) -> None: + with client.logs.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = response.parse() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_get_summary(self, client: CasParser) -> None: + log = client.logs.get_summary() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_get_summary_with_all_params(self, client: CasParser) -> None: + log = client.logs.get_summary( + end_time=parse_datetime("2019-12-27T18:11:19.117Z"), + start_time=parse_datetime("2019-12-27T18:11:19.117Z"), + ) + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_get_summary(self, client: CasParser) -> None: + response = client.logs.with_raw_response.get_summary() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = response.parse() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_get_summary(self, client: CasParser) -> None: + with client.logs.with_streaming_response.get_summary() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = response.parse() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncLogs: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncCasParser) -> None: + log = await async_client.logs.create() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncCasParser) -> None: + log = await async_client.logs.create( + end_time=parse_datetime("2026-01-31T23:59:59Z"), + limit=1, + start_time=parse_datetime("2026-01-01T00:00:00Z"), + ) + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: + response = await async_client.logs.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = await response.parse() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncCasParser) -> None: + async with async_client.logs.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = await response.parse() + assert_matches_type(LogCreateResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_get_summary(self, async_client: AsyncCasParser) -> None: + log = await async_client.logs.get_summary() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_get_summary_with_all_params(self, async_client: AsyncCasParser) -> None: + log = await async_client.logs.get_summary( + end_time=parse_datetime("2019-12-27T18:11:19.117Z"), + start_time=parse_datetime("2019-12-27T18:11:19.117Z"), + ) + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_get_summary(self, async_client: AsyncCasParser) -> None: + response = await async_client.logs.with_raw_response.get_summary() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = await response.parse() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_get_summary(self, async_client: AsyncCasParser) -> None: + async with async_client.logs.with_streaming_response.get_summary() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = await response.parse() + assert_matches_type(LogGetSummaryResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_verify_token.py b/tests/api_resources/test_verify_token.py new file mode 100644 index 0000000..43e594d --- /dev/null +++ b/tests/api_resources/test_verify_token.py @@ -0,0 +1,80 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from cas_parser import CasParser, AsyncCasParser +from tests.utils import assert_matches_type +from cas_parser.types import VerifyTokenVerifyResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestVerifyToken: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_verify(self, client: CasParser) -> None: + verify_token = client.verify_token.verify() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_verify(self, client: CasParser) -> None: + response = client.verify_token.with_raw_response.verify() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + verify_token = response.parse() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_verify(self, client: CasParser) -> None: + with client.verify_token.with_streaming_response.verify() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + verify_token = response.parse() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncVerifyToken: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_verify(self, async_client: AsyncCasParser) -> None: + verify_token = await async_client.verify_token.verify() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_verify(self, async_client: AsyncCasParser) -> None: + response = await async_client.verify_token.with_raw_response.verify() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + verify_token = await response.parse() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_verify(self, async_client: AsyncCasParser) -> None: + async with async_client.verify_token.with_streaming_response.verify() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + verify_token = await response.parse() + assert_matches_type(VerifyTokenVerifyResponse, verify_token, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/test_client.py b/tests/test_client.py index ed6cfac..5a768aa 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -863,20 +863,20 @@ def test_parse_retry_after_header( @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: CasParser) -> None: - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v1/credits").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.cams_kfintech.with_streaming_response.parse().__enter__() + client.credits.with_streaming_response.check().__enter__() assert _get_open_connections(client) == 0 @mock.patch("cas_parser._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: CasParser) -> None: - respx_mock.post("/v4/cams_kfintech/parse").mock(return_value=httpx.Response(500)) + respx_mock.post("/v1/credits").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.cams_kfintech.with_streaming_response.parse().__enter__() + client.credits.with_streaming_response.check().__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -903,9 +903,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v1/credits").mock(side_effect=retry_handler) - response = client.cams_kfintech.with_raw_response.parse() + response = client.credits.with_raw_response.check() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -927,9 +927,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v1/credits").mock(side_effect=retry_handler) - response = client.cams_kfintech.with_raw_response.parse(extra_headers={"x-stainless-retry-count": Omit()}) + response = client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -950,9 +950,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v1/credits").mock(side_effect=retry_handler) - response = client.cams_kfintech.with_raw_response.parse(extra_headers={"x-stainless-retry-count": "42"}) + response = client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1775,10 +1775,10 @@ async def test_parse_retry_after_header( async def test_retrying_timeout_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncCasParser ) -> None: - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v1/credits").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.cams_kfintech.with_streaming_response.parse().__aenter__() + await async_client.credits.with_streaming_response.check().__aenter__() assert _get_open_connections(async_client) == 0 @@ -1787,10 +1787,10 @@ async def test_retrying_timeout_errors_doesnt_leak( async def test_retrying_status_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncCasParser ) -> None: - respx_mock.post("/v4/cams_kfintech/parse").mock(return_value=httpx.Response(500)) + respx_mock.post("/v1/credits").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.cams_kfintech.with_streaming_response.parse().__aenter__() + await async_client.credits.with_streaming_response.check().__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1817,9 +1817,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v1/credits").mock(side_effect=retry_handler) - response = await client.cams_kfintech.with_raw_response.parse() + response = await client.credits.with_raw_response.check() assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1841,9 +1841,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v1/credits").mock(side_effect=retry_handler) - response = await client.cams_kfintech.with_raw_response.parse(extra_headers={"x-stainless-retry-count": Omit()}) + response = await client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1864,9 +1864,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v4/cams_kfintech/parse").mock(side_effect=retry_handler) + respx_mock.post("/v1/credits").mock(side_effect=retry_handler) - response = await client.cams_kfintech.with_raw_response.parse(extra_headers={"x-stainless-retry-count": "42"}) + response = await client.credits.with_raw_response.check(extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From cc091b65bf06460e2b316d76c03aeefba01ba523 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 03:00:49 +0000 Subject: [PATCH 073/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index cdf3c0a..49508b3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-d9763d006969b49a1473851069fdfa429eb13133b64103a62963bb70ddb22305.yml openapi_spec_hash: 6aee689b7a759b12c85c088c15e29bc0 -config_hash: d54f39abb185904495bef7c5f8702746 +config_hash: 4ab3e1ee76a463e0ed214541260ee12e From b69585577accb1d555ba0af5b7cb84f2169bce28 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 03:03:37 +0000 Subject: [PATCH 074/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7325798..fbd9082 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.4.1" + ".": "1.5.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e9c0aea..4e79dc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.4.1" +version = "1.5.0" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index eaa6152..e73e451 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.4.1" # x-release-please-version +__version__ = "1.5.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index 4e4a105..3fbed3d 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.4.1" +version = "1.5.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From b431131575608913e711f4d3db6293d875e44314 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 03:22:17 +0000 Subject: [PATCH 075/116] feat(api): manual updates --- .stats.yml | 2 +- README.md | 6 +-- src/cas_parser/__init__.py | 2 - src/cas_parser/_client.py | 83 ++++++-------------------------------- tests/test_client.py | 24 ----------- 5 files changed, 14 insertions(+), 103 deletions(-) diff --git a/.stats.yml b/.stats.yml index 49508b3..603f57a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-d9763d006969b49a1473851069fdfa429eb13133b64103a62963bb70ddb22305.yml openapi_spec_hash: 6aee689b7a759b12c85c088c15e29bc0 -config_hash: 4ab3e1ee76a463e0ed214541260ee12e +config_hash: 5509bb7a961ae2e79114b24c381606d4 diff --git a/README.md b/README.md index f7b645a..2a98af8 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Use the Cas Parser MCP Server to enable AI assistants to interact with this API, ## Documentation -The REST API documentation can be found on [docs.casparser.in](https://docs.casparser.in). The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found on [casparser.in](https://casparser.in/docs). The full API of this library can be found in [api.md](api.md). ## Installation @@ -39,8 +39,6 @@ from cas_parser import CasParser client = CasParser( api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted - # or 'production' | 'environment_2'; defaults to "production". - environment="environment_1", ) response = client.credits.check() @@ -63,8 +61,6 @@ from cas_parser import AsyncCasParser client = AsyncCasParser( api_key=os.environ.get("CAS_PARSER_API_KEY"), # This is the default and can be omitted - # or 'production' | 'environment_2'; defaults to "production". - environment="environment_1", ) diff --git a/src/cas_parser/__init__.py b/src/cas_parser/__init__.py index a146f39..1e1d246 100644 --- a/src/cas_parser/__init__.py +++ b/src/cas_parser/__init__.py @@ -6,7 +6,6 @@ from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given from ._utils import file_from_path from ._client import ( - ENVIRONMENTS, Client, Stream, Timeout, @@ -74,7 +73,6 @@ "AsyncStream", "CasParser", "AsyncCasParser", - "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py index e63f97f..ebe71c1 100644 --- a/src/cas_parser/_client.py +++ b/src/cas_parser/_client.py @@ -3,8 +3,8 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING, Any, Dict, Mapping, cast -from typing_extensions import Self, Literal, override +from typing import TYPE_CHECKING, Any, Mapping +from typing_extensions import Self, override import httpx @@ -59,7 +59,6 @@ from .resources.inbound_email import InboundEmailResource, AsyncInboundEmailResource __all__ = [ - "ENVIRONMENTS", "Timeout", "Transport", "ProxiesTypes", @@ -70,25 +69,16 @@ "AsyncClient", ] -ENVIRONMENTS: Dict[str, str] = { - "production": "https://portfolio-parser.api.casparser.in", - "environment_1": "https://client-apis.casparser.in", - "environment_2": "http://localhost:5000", -} - class CasParser(SyncAPIClient): # client options api_key: str - _environment: Literal["production", "environment_1", "environment_2"] | NotGiven - def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "environment_1", "environment_2"] | NotGiven = not_given, - base_url: str | httpx.URL | None | NotGiven = not_given, + base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -119,31 +109,10 @@ def __init__( ) self.api_key = api_key - self._environment = environment - - base_url_env = os.environ.get("CAS_PARSER_BASE_URL") - if is_given(base_url) and base_url is not None: - # cast required because mypy doesn't understand the type narrowing - base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] - elif is_given(environment): - if base_url_env and base_url is not None: - raise ValueError( - "Ambiguous URL; The `CAS_PARSER_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", - ) - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc - elif base_url_env is not None: - base_url = base_url_env - else: - self._environment = environment = "production" - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc + if base_url is None: + base_url = os.environ.get("CAS_PARSER_BASE_URL") + if base_url is None: + base_url = f"https://api.casparser.in" super().__init__( version=__version__, @@ -260,7 +229,6 @@ def copy( self, *, api_key: str | None = None, - environment: Literal["production", "environment_1", "environment_2"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.Client | None = None, @@ -296,7 +264,6 @@ def copy( return self.__class__( api_key=api_key or self.api_key, base_url=base_url or self.base_url, - environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, @@ -347,14 +314,11 @@ class AsyncCasParser(AsyncAPIClient): # client options api_key: str - _environment: Literal["production", "environment_1", "environment_2"] | NotGiven - def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "environment_1", "environment_2"] | NotGiven = not_given, - base_url: str | httpx.URL | None | NotGiven = not_given, + base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -385,31 +349,10 @@ def __init__( ) self.api_key = api_key - self._environment = environment - - base_url_env = os.environ.get("CAS_PARSER_BASE_URL") - if is_given(base_url) and base_url is not None: - # cast required because mypy doesn't understand the type narrowing - base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] - elif is_given(environment): - if base_url_env and base_url is not None: - raise ValueError( - "Ambiguous URL; The `CAS_PARSER_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", - ) - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc - elif base_url_env is not None: - base_url = base_url_env - else: - self._environment = environment = "production" - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc + if base_url is None: + base_url = os.environ.get("CAS_PARSER_BASE_URL") + if base_url is None: + base_url = f"https://api.casparser.in" super().__init__( version=__version__, @@ -526,7 +469,6 @@ def copy( self, *, api_key: str | None = None, - environment: Literal["production", "environment_1", "environment_2"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = not_given, http_client: httpx.AsyncClient | None = None, @@ -562,7 +504,6 @@ def copy( return self.__class__( api_key=api_key or self.api_key, base_url=base_url or self.base_url, - environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, diff --git a/tests/test_client.py b/tests/test_client.py index 5a768aa..ec19747 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -691,18 +691,6 @@ def test_base_url_env(self) -> None: client = CasParser(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" - # explicit environment arg requires explicitness - with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): - with pytest.raises(ValueError, match=r"you must pass base_url=None"): - CasParser(api_key=api_key, _strict_response_validation=True, environment="production") - - client = CasParser( - base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" - ) - assert str(client.base_url).startswith("https://portfolio-parser.api.casparser.in") - - client.close() - @pytest.mark.parametrize( "client", [ @@ -1592,18 +1580,6 @@ async def test_base_url_env(self) -> None: client = AsyncCasParser(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" - # explicit environment arg requires explicitness - with update_env(CAS_PARSER_BASE_URL="http://localhost:5000/from/env"): - with pytest.raises(ValueError, match=r"you must pass base_url=None"): - AsyncCasParser(api_key=api_key, _strict_response_validation=True, environment="production") - - client = AsyncCasParser( - base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" - ) - assert str(client.base_url).startswith("https://portfolio-parser.api.casparser.in") - - await client.close() - @pytest.mark.parametrize( "client", [ From f82229bf69e5c8a9c612f5c83110049f0f84a9e7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 03:26:05 +0000 Subject: [PATCH 076/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fbd9082..7deae33 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.5.0" + ".": "1.6.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4e79dc8..85106e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.5.0" +version = "1.6.0" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index e73e451..1d9ca9a 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.5.0" # x-release-please-version +__version__ = "1.6.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index 3fbed3d..9339e2e 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.5.0" +version = "1.6.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From 20ab320d41ae43a08916fe7a4435aff23386aa56 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 05:42:18 +0000 Subject: [PATCH 077/116] chore(internal): add request options to SSE classes --- src/cas_parser/_response.py | 3 +++ src/cas_parser/_streaming.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/cas_parser/_response.py b/src/cas_parser/_response.py index a67d3f1..66c0f1a 100644 --- a/src/cas_parser/_response.py +++ b/src/cas_parser/_response.py @@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/cas_parser/_streaming.py b/src/cas_parser/_streaming.py index 00e2105..121e6be 100644 --- a/src/cas_parser/_streaming.py +++ b/src/cas_parser/_streaming.py @@ -4,7 +4,7 @@ import json import inspect from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable import httpx @@ -13,6 +13,7 @@ if TYPE_CHECKING: from ._client import CasParser, AsyncCasParser + from ._models import FinalRequestOptions _T = TypeVar("_T") @@ -22,7 +23,7 @@ class Stream(Generic[_T]): """Provides the core interface to iterate over a synchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEBytesDecoder def __init__( @@ -31,10 +32,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: CasParser, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() @@ -85,7 +88,7 @@ class AsyncStream(Generic[_T]): """Provides the core interface to iterate over an asynchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEDecoder | SSEBytesDecoder def __init__( @@ -94,10 +97,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncCasParser, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() From 440b8e0c7c69f27d2d50abe6ba84230b79c33af6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 05:52:45 +0000 Subject: [PATCH 078/116] chore(internal): make `test_proxy_environment_variables` more resilient --- tests/test_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index ec19747..b0b6525 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -947,6 +947,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultHttpxClient() @@ -1853,6 +1855,8 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultAsyncHttpxClient() From d23fe247eed56b5c85b37b03b38259bab0835539 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 05:25:54 +0000 Subject: [PATCH 079/116] chore(internal): make `test_proxy_environment_variables` more resilient to env --- tests/test_client.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index b0b6525..9e2377f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -947,8 +947,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultHttpxClient() @@ -1855,8 +1861,14 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultAsyncHttpxClient() From 89abab831b230646e17a1f4a7a593c65b427ad14 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 06:32:05 +0000 Subject: [PATCH 080/116] chore(ci): bump uv version --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbbfbc2..0b10752 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 with: - version: '0.9.13' + version: '0.10.2' - name: Install dependencies run: uv sync --all-extras @@ -46,7 +46,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 with: - version: '0.9.13' + version: '0.10.2' - name: Install dependencies run: uv sync --all-extras @@ -80,7 +80,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 with: - version: '0.9.13' + version: '0.10.2' - name: Bootstrap run: ./scripts/bootstrap From 8b0172da97930ec4abb3673633a8f3e95375ffe1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:19:41 +0000 Subject: [PATCH 081/116] chore(internal): codegen related update --- src/cas_parser/_client.py | 384 ++++++++++++++++++++++ src/cas_parser/resources/access_token.py | 12 + src/cas_parser/resources/cams_kfintech.py | 4 + src/cas_parser/resources/cdsl/cdsl.py | 28 ++ src/cas_parser/resources/cdsl/fetch.py | 10 + src/cas_parser/resources/contract_note.py | 8 + src/cas_parser/resources/credits.py | 10 + src/cas_parser/resources/inbound_email.py | 46 +++ src/cas_parser/resources/inbox.py | 34 ++ src/cas_parser/resources/kfintech.py | 4 + src/cas_parser/resources/logs.py | 10 + src/cas_parser/resources/nsdl.py | 4 + src/cas_parser/resources/smart.py | 4 + src/cas_parser/resources/verify_token.py | 12 + 14 files changed, 570 insertions(+) diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py index ebe71c1..bc29c05 100644 --- a/src/cas_parser/_client.py +++ b/src/cas_parser/_client.py @@ -127,72 +127,136 @@ def __init__( @cached_property def credits(self) -> CreditsResource: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.credits import CreditsResource return CreditsResource(self) @cached_property def logs(self) -> LogsResource: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.logs import LogsResource return LogsResource(self) @cached_property def access_token(self) -> AccessTokenResource: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.access_token import AccessTokenResource return AccessTokenResource(self) @cached_property def verify_token(self) -> VerifyTokenResource: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.verify_token import VerifyTokenResource return VerifyTokenResource(self) @cached_property def cams_kfintech(self) -> CamsKfintechResource: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cams_kfintech import CamsKfintechResource return CamsKfintechResource(self) @cached_property def cdsl(self) -> CdslResource: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cdsl import CdslResource return CdslResource(self) @cached_property def contract_note(self) -> ContractNoteResource: + """ + Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww, Upstox, ICICI etc. + """ from .resources.contract_note import ContractNoteResource return ContractNoteResource(self) @cached_property def inbox(self) -> InboxResource: + """Endpoints for importing CAS files directly from user email inboxes. + + **Supported Providers:** Gmail (more coming soon) + + **How it works:** + 1. Call `POST /v4/inbox/connect` to get an OAuth URL + 2. Redirect user to the OAuth URL for consent + 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token` + 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`) + 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours) + + **Security:** + - Read-only access (we cannot send emails) + - Tokens are encrypted with server-side secret + - User can revoke access anytime via `/v4/inbox/disconnect` + """ from .resources.inbox import InboxResource return InboxResource(self) @cached_property def kfintech(self) -> KfintechResource: + """Endpoints for generating new CAS documents via email mailback (KFintech).""" from .resources.kfintech import KfintechResource return KfintechResource(self) @cached_property def nsdl(self) -> NsdlResource: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.nsdl import NsdlResource return NsdlResource(self) @cached_property def smart(self) -> SmartResource: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.smart import SmartResource return SmartResource(self) @cached_property def inbound_email(self) -> InboundEmailResource: + """ + Create dedicated inbound email addresses for investors to forward their CAS statements. + + **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or file upload. + + **How it works:** + 1. Call `POST /v4/inbound-email` to create a unique inbound email address + 2. Display this email to your user: "Forward your CAS statement to ie_xxx@import.casparser.in" + 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your webhook + 4. Your webhook receives email metadata + attachment download URLs + + **Sender Validation:** + - Only emails from verified CAS authorities are processed: + - CDSL: `eCAS@cdslstatement.com` + - NSDL: `NSDL-CAS@nsdl.co.in` + - CAMS: `donotreply@camsonline.com` + - KFintech: `samfS@kfintech.com` + - Emails failing SPF/DKIM/DMARC are rejected + - Forwarded emails must contain the original sender in headers + + **Billing:** 0.2 credits per successfully processed valid email + """ from .resources.inbound_email import InboundEmailResource return InboundEmailResource(self) @@ -367,72 +431,136 @@ def __init__( @cached_property def credits(self) -> AsyncCreditsResource: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.credits import AsyncCreditsResource return AsyncCreditsResource(self) @cached_property def logs(self) -> AsyncLogsResource: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.logs import AsyncLogsResource return AsyncLogsResource(self) @cached_property def access_token(self) -> AsyncAccessTokenResource: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.access_token import AsyncAccessTokenResource return AsyncAccessTokenResource(self) @cached_property def verify_token(self) -> AsyncVerifyTokenResource: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.verify_token import AsyncVerifyTokenResource return AsyncVerifyTokenResource(self) @cached_property def cams_kfintech(self) -> AsyncCamsKfintechResource: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cams_kfintech import AsyncCamsKfintechResource return AsyncCamsKfintechResource(self) @cached_property def cdsl(self) -> AsyncCdslResource: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cdsl import AsyncCdslResource return AsyncCdslResource(self) @cached_property def contract_note(self) -> AsyncContractNoteResource: + """ + Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww, Upstox, ICICI etc. + """ from .resources.contract_note import AsyncContractNoteResource return AsyncContractNoteResource(self) @cached_property def inbox(self) -> AsyncInboxResource: + """Endpoints for importing CAS files directly from user email inboxes. + + **Supported Providers:** Gmail (more coming soon) + + **How it works:** + 1. Call `POST /v4/inbox/connect` to get an OAuth URL + 2. Redirect user to the OAuth URL for consent + 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token` + 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`) + 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours) + + **Security:** + - Read-only access (we cannot send emails) + - Tokens are encrypted with server-side secret + - User can revoke access anytime via `/v4/inbox/disconnect` + """ from .resources.inbox import AsyncInboxResource return AsyncInboxResource(self) @cached_property def kfintech(self) -> AsyncKfintechResource: + """Endpoints for generating new CAS documents via email mailback (KFintech).""" from .resources.kfintech import AsyncKfintechResource return AsyncKfintechResource(self) @cached_property def nsdl(self) -> AsyncNsdlResource: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.nsdl import AsyncNsdlResource return AsyncNsdlResource(self) @cached_property def smart(self) -> AsyncSmartResource: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.smart import AsyncSmartResource return AsyncSmartResource(self) @cached_property def inbound_email(self) -> AsyncInboundEmailResource: + """ + Create dedicated inbound email addresses for investors to forward their CAS statements. + + **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or file upload. + + **How it works:** + 1. Call `POST /v4/inbound-email` to create a unique inbound email address + 2. Display this email to your user: "Forward your CAS statement to ie_xxx@import.casparser.in" + 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your webhook + 4. Your webhook receives email metadata + attachment download URLs + + **Sender Validation:** + - Only emails from verified CAS authorities are processed: + - CDSL: `eCAS@cdslstatement.com` + - NSDL: `NSDL-CAS@nsdl.co.in` + - CAMS: `donotreply@camsonline.com` + - KFintech: `samfS@kfintech.com` + - Emails failing SPF/DKIM/DMARC are rejected + - Forwarded emails must contain the original sender in headers + + **Billing:** 0.2 credits per successfully processed valid email + """ from .resources.inbound_email import AsyncInboundEmailResource return AsyncInboundEmailResource(self) @@ -558,72 +686,136 @@ def __init__(self, client: CasParser) -> None: @cached_property def credits(self) -> credits.CreditsResourceWithRawResponse: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.credits import CreditsResourceWithRawResponse return CreditsResourceWithRawResponse(self._client.credits) @cached_property def logs(self) -> logs.LogsResourceWithRawResponse: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.logs import LogsResourceWithRawResponse return LogsResourceWithRawResponse(self._client.logs) @cached_property def access_token(self) -> access_token.AccessTokenResourceWithRawResponse: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.access_token import AccessTokenResourceWithRawResponse return AccessTokenResourceWithRawResponse(self._client.access_token) @cached_property def verify_token(self) -> verify_token.VerifyTokenResourceWithRawResponse: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.verify_token import VerifyTokenResourceWithRawResponse return VerifyTokenResourceWithRawResponse(self._client.verify_token) @cached_property def cams_kfintech(self) -> cams_kfintech.CamsKfintechResourceWithRawResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cams_kfintech import CamsKfintechResourceWithRawResponse return CamsKfintechResourceWithRawResponse(self._client.cams_kfintech) @cached_property def cdsl(self) -> cdsl.CdslResourceWithRawResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cdsl import CdslResourceWithRawResponse return CdslResourceWithRawResponse(self._client.cdsl) @cached_property def contract_note(self) -> contract_note.ContractNoteResourceWithRawResponse: + """ + Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww, Upstox, ICICI etc. + """ from .resources.contract_note import ContractNoteResourceWithRawResponse return ContractNoteResourceWithRawResponse(self._client.contract_note) @cached_property def inbox(self) -> inbox.InboxResourceWithRawResponse: + """Endpoints for importing CAS files directly from user email inboxes. + + **Supported Providers:** Gmail (more coming soon) + + **How it works:** + 1. Call `POST /v4/inbox/connect` to get an OAuth URL + 2. Redirect user to the OAuth URL for consent + 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token` + 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`) + 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours) + + **Security:** + - Read-only access (we cannot send emails) + - Tokens are encrypted with server-side secret + - User can revoke access anytime via `/v4/inbox/disconnect` + """ from .resources.inbox import InboxResourceWithRawResponse return InboxResourceWithRawResponse(self._client.inbox) @cached_property def kfintech(self) -> kfintech.KfintechResourceWithRawResponse: + """Endpoints for generating new CAS documents via email mailback (KFintech).""" from .resources.kfintech import KfintechResourceWithRawResponse return KfintechResourceWithRawResponse(self._client.kfintech) @cached_property def nsdl(self) -> nsdl.NsdlResourceWithRawResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.nsdl import NsdlResourceWithRawResponse return NsdlResourceWithRawResponse(self._client.nsdl) @cached_property def smart(self) -> smart.SmartResourceWithRawResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.smart import SmartResourceWithRawResponse return SmartResourceWithRawResponse(self._client.smart) @cached_property def inbound_email(self) -> inbound_email.InboundEmailResourceWithRawResponse: + """ + Create dedicated inbound email addresses for investors to forward their CAS statements. + + **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or file upload. + + **How it works:** + 1. Call `POST /v4/inbound-email` to create a unique inbound email address + 2. Display this email to your user: "Forward your CAS statement to ie_xxx@import.casparser.in" + 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your webhook + 4. Your webhook receives email metadata + attachment download URLs + + **Sender Validation:** + - Only emails from verified CAS authorities are processed: + - CDSL: `eCAS@cdslstatement.com` + - NSDL: `NSDL-CAS@nsdl.co.in` + - CAMS: `donotreply@camsonline.com` + - KFintech: `samfS@kfintech.com` + - Emails failing SPF/DKIM/DMARC are rejected + - Forwarded emails must contain the original sender in headers + + **Billing:** 0.2 credits per successfully processed valid email + """ from .resources.inbound_email import InboundEmailResourceWithRawResponse return InboundEmailResourceWithRawResponse(self._client.inbound_email) @@ -637,72 +829,136 @@ def __init__(self, client: AsyncCasParser) -> None: @cached_property def credits(self) -> credits.AsyncCreditsResourceWithRawResponse: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.credits import AsyncCreditsResourceWithRawResponse return AsyncCreditsResourceWithRawResponse(self._client.credits) @cached_property def logs(self) -> logs.AsyncLogsResourceWithRawResponse: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.logs import AsyncLogsResourceWithRawResponse return AsyncLogsResourceWithRawResponse(self._client.logs) @cached_property def access_token(self) -> access_token.AsyncAccessTokenResourceWithRawResponse: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.access_token import AsyncAccessTokenResourceWithRawResponse return AsyncAccessTokenResourceWithRawResponse(self._client.access_token) @cached_property def verify_token(self) -> verify_token.AsyncVerifyTokenResourceWithRawResponse: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.verify_token import AsyncVerifyTokenResourceWithRawResponse return AsyncVerifyTokenResourceWithRawResponse(self._client.verify_token) @cached_property def cams_kfintech(self) -> cams_kfintech.AsyncCamsKfintechResourceWithRawResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cams_kfintech import AsyncCamsKfintechResourceWithRawResponse return AsyncCamsKfintechResourceWithRawResponse(self._client.cams_kfintech) @cached_property def cdsl(self) -> cdsl.AsyncCdslResourceWithRawResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cdsl import AsyncCdslResourceWithRawResponse return AsyncCdslResourceWithRawResponse(self._client.cdsl) @cached_property def contract_note(self) -> contract_note.AsyncContractNoteResourceWithRawResponse: + """ + Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww, Upstox, ICICI etc. + """ from .resources.contract_note import AsyncContractNoteResourceWithRawResponse return AsyncContractNoteResourceWithRawResponse(self._client.contract_note) @cached_property def inbox(self) -> inbox.AsyncInboxResourceWithRawResponse: + """Endpoints for importing CAS files directly from user email inboxes. + + **Supported Providers:** Gmail (more coming soon) + + **How it works:** + 1. Call `POST /v4/inbox/connect` to get an OAuth URL + 2. Redirect user to the OAuth URL for consent + 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token` + 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`) + 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours) + + **Security:** + - Read-only access (we cannot send emails) + - Tokens are encrypted with server-side secret + - User can revoke access anytime via `/v4/inbox/disconnect` + """ from .resources.inbox import AsyncInboxResourceWithRawResponse return AsyncInboxResourceWithRawResponse(self._client.inbox) @cached_property def kfintech(self) -> kfintech.AsyncKfintechResourceWithRawResponse: + """Endpoints for generating new CAS documents via email mailback (KFintech).""" from .resources.kfintech import AsyncKfintechResourceWithRawResponse return AsyncKfintechResourceWithRawResponse(self._client.kfintech) @cached_property def nsdl(self) -> nsdl.AsyncNsdlResourceWithRawResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.nsdl import AsyncNsdlResourceWithRawResponse return AsyncNsdlResourceWithRawResponse(self._client.nsdl) @cached_property def smart(self) -> smart.AsyncSmartResourceWithRawResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.smart import AsyncSmartResourceWithRawResponse return AsyncSmartResourceWithRawResponse(self._client.smart) @cached_property def inbound_email(self) -> inbound_email.AsyncInboundEmailResourceWithRawResponse: + """ + Create dedicated inbound email addresses for investors to forward their CAS statements. + + **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or file upload. + + **How it works:** + 1. Call `POST /v4/inbound-email` to create a unique inbound email address + 2. Display this email to your user: "Forward your CAS statement to ie_xxx@import.casparser.in" + 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your webhook + 4. Your webhook receives email metadata + attachment download URLs + + **Sender Validation:** + - Only emails from verified CAS authorities are processed: + - CDSL: `eCAS@cdslstatement.com` + - NSDL: `NSDL-CAS@nsdl.co.in` + - CAMS: `donotreply@camsonline.com` + - KFintech: `samfS@kfintech.com` + - Emails failing SPF/DKIM/DMARC are rejected + - Forwarded emails must contain the original sender in headers + + **Billing:** 0.2 credits per successfully processed valid email + """ from .resources.inbound_email import AsyncInboundEmailResourceWithRawResponse return AsyncInboundEmailResourceWithRawResponse(self._client.inbound_email) @@ -716,72 +972,136 @@ def __init__(self, client: CasParser) -> None: @cached_property def credits(self) -> credits.CreditsResourceWithStreamingResponse: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.credits import CreditsResourceWithStreamingResponse return CreditsResourceWithStreamingResponse(self._client.credits) @cached_property def logs(self) -> logs.LogsResourceWithStreamingResponse: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.logs import LogsResourceWithStreamingResponse return LogsResourceWithStreamingResponse(self._client.logs) @cached_property def access_token(self) -> access_token.AccessTokenResourceWithStreamingResponse: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.access_token import AccessTokenResourceWithStreamingResponse return AccessTokenResourceWithStreamingResponse(self._client.access_token) @cached_property def verify_token(self) -> verify_token.VerifyTokenResourceWithStreamingResponse: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.verify_token import VerifyTokenResourceWithStreamingResponse return VerifyTokenResourceWithStreamingResponse(self._client.verify_token) @cached_property def cams_kfintech(self) -> cams_kfintech.CamsKfintechResourceWithStreamingResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cams_kfintech import CamsKfintechResourceWithStreamingResponse return CamsKfintechResourceWithStreamingResponse(self._client.cams_kfintech) @cached_property def cdsl(self) -> cdsl.CdslResourceWithStreamingResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cdsl import CdslResourceWithStreamingResponse return CdslResourceWithStreamingResponse(self._client.cdsl) @cached_property def contract_note(self) -> contract_note.ContractNoteResourceWithStreamingResponse: + """ + Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww, Upstox, ICICI etc. + """ from .resources.contract_note import ContractNoteResourceWithStreamingResponse return ContractNoteResourceWithStreamingResponse(self._client.contract_note) @cached_property def inbox(self) -> inbox.InboxResourceWithStreamingResponse: + """Endpoints for importing CAS files directly from user email inboxes. + + **Supported Providers:** Gmail (more coming soon) + + **How it works:** + 1. Call `POST /v4/inbox/connect` to get an OAuth URL + 2. Redirect user to the OAuth URL for consent + 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token` + 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`) + 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours) + + **Security:** + - Read-only access (we cannot send emails) + - Tokens are encrypted with server-side secret + - User can revoke access anytime via `/v4/inbox/disconnect` + """ from .resources.inbox import InboxResourceWithStreamingResponse return InboxResourceWithStreamingResponse(self._client.inbox) @cached_property def kfintech(self) -> kfintech.KfintechResourceWithStreamingResponse: + """Endpoints for generating new CAS documents via email mailback (KFintech).""" from .resources.kfintech import KfintechResourceWithStreamingResponse return KfintechResourceWithStreamingResponse(self._client.kfintech) @cached_property def nsdl(self) -> nsdl.NsdlResourceWithStreamingResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.nsdl import NsdlResourceWithStreamingResponse return NsdlResourceWithStreamingResponse(self._client.nsdl) @cached_property def smart(self) -> smart.SmartResourceWithStreamingResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.smart import SmartResourceWithStreamingResponse return SmartResourceWithStreamingResponse(self._client.smart) @cached_property def inbound_email(self) -> inbound_email.InboundEmailResourceWithStreamingResponse: + """ + Create dedicated inbound email addresses for investors to forward their CAS statements. + + **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or file upload. + + **How it works:** + 1. Call `POST /v4/inbound-email` to create a unique inbound email address + 2. Display this email to your user: "Forward your CAS statement to ie_xxx@import.casparser.in" + 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your webhook + 4. Your webhook receives email metadata + attachment download URLs + + **Sender Validation:** + - Only emails from verified CAS authorities are processed: + - CDSL: `eCAS@cdslstatement.com` + - NSDL: `NSDL-CAS@nsdl.co.in` + - CAMS: `donotreply@camsonline.com` + - KFintech: `samfS@kfintech.com` + - Emails failing SPF/DKIM/DMARC are rejected + - Forwarded emails must contain the original sender in headers + + **Billing:** 0.2 credits per successfully processed valid email + """ from .resources.inbound_email import InboundEmailResourceWithStreamingResponse return InboundEmailResourceWithStreamingResponse(self._client.inbound_email) @@ -795,72 +1115,136 @@ def __init__(self, client: AsyncCasParser) -> None: @cached_property def credits(self) -> credits.AsyncCreditsResourceWithStreamingResponse: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.credits import AsyncCreditsResourceWithStreamingResponse return AsyncCreditsResourceWithStreamingResponse(self._client.credits) @cached_property def logs(self) -> logs.AsyncLogsResourceWithStreamingResponse: + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ from .resources.logs import AsyncLogsResourceWithStreamingResponse return AsyncLogsResourceWithStreamingResponse(self._client.logs) @cached_property def access_token(self) -> access_token.AsyncAccessTokenResourceWithStreamingResponse: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.access_token import AsyncAccessTokenResourceWithStreamingResponse return AsyncAccessTokenResourceWithStreamingResponse(self._client.access_token) @cached_property def verify_token(self) -> verify_token.AsyncVerifyTokenResourceWithStreamingResponse: + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ from .resources.verify_token import AsyncVerifyTokenResourceWithStreamingResponse return AsyncVerifyTokenResourceWithStreamingResponse(self._client.verify_token) @cached_property def cams_kfintech(self) -> cams_kfintech.AsyncCamsKfintechResourceWithStreamingResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cams_kfintech import AsyncCamsKfintechResourceWithStreamingResponse return AsyncCamsKfintechResourceWithStreamingResponse(self._client.cams_kfintech) @cached_property def cdsl(self) -> cdsl.AsyncCdslResourceWithStreamingResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.cdsl import AsyncCdslResourceWithStreamingResponse return AsyncCdslResourceWithStreamingResponse(self._client.cdsl) @cached_property def contract_note(self) -> contract_note.AsyncContractNoteResourceWithStreamingResponse: + """ + Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww, Upstox, ICICI etc. + """ from .resources.contract_note import AsyncContractNoteResourceWithStreamingResponse return AsyncContractNoteResourceWithStreamingResponse(self._client.contract_note) @cached_property def inbox(self) -> inbox.AsyncInboxResourceWithStreamingResponse: + """Endpoints for importing CAS files directly from user email inboxes. + + **Supported Providers:** Gmail (more coming soon) + + **How it works:** + 1. Call `POST /v4/inbox/connect` to get an OAuth URL + 2. Redirect user to the OAuth URL for consent + 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token` + 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`) + 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours) + + **Security:** + - Read-only access (we cannot send emails) + - Tokens are encrypted with server-side secret + - User can revoke access anytime via `/v4/inbox/disconnect` + """ from .resources.inbox import AsyncInboxResourceWithStreamingResponse return AsyncInboxResourceWithStreamingResponse(self._client.inbox) @cached_property def kfintech(self) -> kfintech.AsyncKfintechResourceWithStreamingResponse: + """Endpoints for generating new CAS documents via email mailback (KFintech).""" from .resources.kfintech import AsyncKfintechResourceWithStreamingResponse return AsyncKfintechResourceWithStreamingResponse(self._client.kfintech) @cached_property def nsdl(self) -> nsdl.AsyncNsdlResourceWithStreamingResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.nsdl import AsyncNsdlResourceWithStreamingResponse return AsyncNsdlResourceWithStreamingResponse(self._client.nsdl) @cached_property def smart(self) -> smart.AsyncSmartResourceWithStreamingResponse: + """Endpoints for parsing CAS PDF files from different sources.""" from .resources.smart import AsyncSmartResourceWithStreamingResponse return AsyncSmartResourceWithStreamingResponse(self._client.smart) @cached_property def inbound_email(self) -> inbound_email.AsyncInboundEmailResourceWithStreamingResponse: + """ + Create dedicated inbound email addresses for investors to forward their CAS statements. + + **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or file upload. + + **How it works:** + 1. Call `POST /v4/inbound-email` to create a unique inbound email address + 2. Display this email to your user: "Forward your CAS statement to ie_xxx@import.casparser.in" + 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your webhook + 4. Your webhook receives email metadata + attachment download URLs + + **Sender Validation:** + - Only emails from verified CAS authorities are processed: + - CDSL: `eCAS@cdslstatement.com` + - NSDL: `NSDL-CAS@nsdl.co.in` + - CAMS: `donotreply@camsonline.com` + - KFintech: `samfS@kfintech.com` + - Emails failing SPF/DKIM/DMARC are rejected + - Forwarded emails must contain the original sender in headers + + **Billing:** 0.2 credits per successfully processed valid email + """ from .resources.inbound_email import AsyncInboundEmailResourceWithStreamingResponse return AsyncInboundEmailResourceWithStreamingResponse(self._client.inbound_email) diff --git a/src/cas_parser/resources/access_token.py b/src/cas_parser/resources/access_token.py index bfda93b..fcf841c 100644 --- a/src/cas_parser/resources/access_token.py +++ b/src/cas_parser/resources/access_token.py @@ -22,6 +22,12 @@ class AccessTokenResource(SyncAPIResource): + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ + @cached_property def with_raw_response(self) -> AccessTokenResourceWithRawResponse: """ @@ -91,6 +97,12 @@ def create( class AsyncAccessTokenResource(AsyncAPIResource): + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ + @cached_property def with_raw_response(self) -> AsyncAccessTokenResourceWithRawResponse: """ diff --git a/src/cas_parser/resources/cams_kfintech.py b/src/cas_parser/resources/cams_kfintech.py index 04ef754..fb32699 100644 --- a/src/cas_parser/resources/cams_kfintech.py +++ b/src/cas_parser/resources/cams_kfintech.py @@ -24,6 +24,8 @@ class CamsKfintechResource(SyncAPIResource): + """Endpoints for parsing CAS PDF files from different sources.""" + @cached_property def with_raw_response(self) -> CamsKfintechResourceWithRawResponse: """ @@ -101,6 +103,8 @@ def parse( class AsyncCamsKfintechResource(AsyncAPIResource): + """Endpoints for parsing CAS PDF files from different sources.""" + @cached_property def with_raw_response(self) -> AsyncCamsKfintechResourceWithRawResponse: """ diff --git a/src/cas_parser/resources/cdsl/cdsl.py b/src/cas_parser/resources/cdsl/cdsl.py index d3b39ff..d0c69b9 100644 --- a/src/cas_parser/resources/cdsl/cdsl.py +++ b/src/cas_parser/resources/cdsl/cdsl.py @@ -32,8 +32,14 @@ class CdslResource(SyncAPIResource): + """Endpoints for parsing CAS PDF files from different sources.""" + @cached_property def fetch(self) -> FetchResource: + """ + Endpoints for fetching CAS documents with instant download. + Currently supports CDSL via OTP authentication. + """ return FetchResource(self._client) @cached_property @@ -113,8 +119,14 @@ def parse_pdf( class AsyncCdslResource(AsyncAPIResource): + """Endpoints for parsing CAS PDF files from different sources.""" + @cached_property def fetch(self) -> AsyncFetchResource: + """ + Endpoints for fetching CAS documents with instant download. + Currently supports CDSL via OTP authentication. + """ return AsyncFetchResource(self._client) @cached_property @@ -203,6 +215,10 @@ def __init__(self, cdsl: CdslResource) -> None: @cached_property def fetch(self) -> FetchResourceWithRawResponse: + """ + Endpoints for fetching CAS documents with instant download. + Currently supports CDSL via OTP authentication. + """ return FetchResourceWithRawResponse(self._cdsl.fetch) @@ -216,6 +232,10 @@ def __init__(self, cdsl: AsyncCdslResource) -> None: @cached_property def fetch(self) -> AsyncFetchResourceWithRawResponse: + """ + Endpoints for fetching CAS documents with instant download. + Currently supports CDSL via OTP authentication. + """ return AsyncFetchResourceWithRawResponse(self._cdsl.fetch) @@ -229,6 +249,10 @@ def __init__(self, cdsl: CdslResource) -> None: @cached_property def fetch(self) -> FetchResourceWithStreamingResponse: + """ + Endpoints for fetching CAS documents with instant download. + Currently supports CDSL via OTP authentication. + """ return FetchResourceWithStreamingResponse(self._cdsl.fetch) @@ -242,4 +266,8 @@ def __init__(self, cdsl: AsyncCdslResource) -> None: @cached_property def fetch(self) -> AsyncFetchResourceWithStreamingResponse: + """ + Endpoints for fetching CAS documents with instant download. + Currently supports CDSL via OTP authentication. + """ return AsyncFetchResourceWithStreamingResponse(self._cdsl.fetch) diff --git a/src/cas_parser/resources/cdsl/fetch.py b/src/cas_parser/resources/cdsl/fetch.py index abbb6a1..191c3a7 100644 --- a/src/cas_parser/resources/cdsl/fetch.py +++ b/src/cas_parser/resources/cdsl/fetch.py @@ -23,6 +23,11 @@ class FetchResource(SyncAPIResource): + """ + Endpoints for fetching CAS documents with instant download. + Currently supports CDSL via OTP authentication. + """ + @cached_property def with_raw_response(self) -> FetchResourceWithRawResponse: """ @@ -149,6 +154,11 @@ def verify_otp( class AsyncFetchResource(AsyncAPIResource): + """ + Endpoints for fetching CAS documents with instant download. + Currently supports CDSL via OTP authentication. + """ + @cached_property def with_raw_response(self) -> AsyncFetchResourceWithRawResponse: """ diff --git a/src/cas_parser/resources/contract_note.py b/src/cas_parser/resources/contract_note.py index 5136c68..7133be7 100644 --- a/src/cas_parser/resources/contract_note.py +++ b/src/cas_parser/resources/contract_note.py @@ -25,6 +25,10 @@ class ContractNoteResource(SyncAPIResource): + """ + Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww, Upstox, ICICI etc. + """ + @cached_property def with_raw_response(self) -> ContractNoteResourceWithRawResponse: """ @@ -132,6 +136,10 @@ def parse( class AsyncContractNoteResource(AsyncAPIResource): + """ + Endpoints for parsing Contract Note PDF files from various SEBI brokers like Zerodha, Groww, Upstox, ICICI etc. + """ + @cached_property def with_raw_response(self) -> AsyncContractNoteResourceWithRawResponse: """ diff --git a/src/cas_parser/resources/credits.py b/src/cas_parser/resources/credits.py index 264693d..0553441 100644 --- a/src/cas_parser/resources/credits.py +++ b/src/cas_parser/resources/credits.py @@ -20,6 +20,11 @@ class CreditsResource(SyncAPIResource): + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ + @cached_property def with_raw_response(self) -> CreditsResourceWithRawResponse: """ @@ -70,6 +75,11 @@ def check( class AsyncCreditsResource(AsyncAPIResource): + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ + @cached_property def with_raw_response(self) -> AsyncCreditsResourceWithRawResponse: """ diff --git a/src/cas_parser/resources/inbound_email.py b/src/cas_parser/resources/inbound_email.py index e20ae7b..3e45f5d 100644 --- a/src/cas_parser/resources/inbound_email.py +++ b/src/cas_parser/resources/inbound_email.py @@ -28,6 +28,29 @@ class InboundEmailResource(SyncAPIResource): + """ + Create dedicated inbound email addresses for investors to forward their CAS statements. + + **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or file upload. + + **How it works:** + 1. Call `POST /v4/inbound-email` to create a unique inbound email address + 2. Display this email to your user: "Forward your CAS statement to ie_xxx@import.casparser.in" + 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your webhook + 4. Your webhook receives email metadata + attachment download URLs + + **Sender Validation:** + - Only emails from verified CAS authorities are processed: + - CDSL: `eCAS@cdslstatement.com` + - NSDL: `NSDL-CAS@nsdl.co.in` + - CAMS: `donotreply@camsonline.com` + - KFintech: `samfS@kfintech.com` + - Emails failing SPF/DKIM/DMARC are rejected + - Forwarded emails must contain the original sender in headers + + **Billing:** 0.2 credits per successfully processed valid email + """ + @cached_property def with_raw_response(self) -> InboundEmailResourceWithRawResponse: """ @@ -260,6 +283,29 @@ def delete( class AsyncInboundEmailResource(AsyncAPIResource): + """ + Create dedicated inbound email addresses for investors to forward their CAS statements. + + **Use Case:** Your app wants to collect CAS statements from users without requiring OAuth or file upload. + + **How it works:** + 1. Call `POST /v4/inbound-email` to create a unique inbound email address + 2. Display this email to your user: "Forward your CAS statement to ie_xxx@import.casparser.in" + 3. When user forwards a CAS email, we verify sender authenticity (SPF/DKIM) and call your webhook + 4. Your webhook receives email metadata + attachment download URLs + + **Sender Validation:** + - Only emails from verified CAS authorities are processed: + - CDSL: `eCAS@cdslstatement.com` + - NSDL: `NSDL-CAS@nsdl.co.in` + - CAMS: `donotreply@camsonline.com` + - KFintech: `samfS@kfintech.com` + - Emails failing SPF/DKIM/DMARC are rejected + - Forwarded emails must contain the original sender in headers + + **Billing:** 0.2 credits per successfully processed valid email + """ + @cached_property def with_raw_response(self) -> AsyncInboundEmailResourceWithRawResponse: """ diff --git a/src/cas_parser/resources/inbox.py b/src/cas_parser/resources/inbox.py index dd8e8c9..7692744 100644 --- a/src/cas_parser/resources/inbox.py +++ b/src/cas_parser/resources/inbox.py @@ -29,6 +29,23 @@ class InboxResource(SyncAPIResource): + """Endpoints for importing CAS files directly from user email inboxes. + + **Supported Providers:** Gmail (more coming soon) + + **How it works:** + 1. Call `POST /v4/inbox/connect` to get an OAuth URL + 2. Redirect user to the OAuth URL for consent + 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token` + 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`) + 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours) + + **Security:** + - Read-only access (we cannot send emails) + - Tokens are encrypted with server-side secret + - User can revoke access anytime via `/v4/inbox/disconnect` + """ + @cached_property def with_raw_response(self) -> InboxResourceWithRawResponse: """ @@ -246,6 +263,23 @@ def list_cas_files( class AsyncInboxResource(AsyncAPIResource): + """Endpoints for importing CAS files directly from user email inboxes. + + **Supported Providers:** Gmail (more coming soon) + + **How it works:** + 1. Call `POST /v4/inbox/connect` to get an OAuth URL + 2. Redirect user to the OAuth URL for consent + 3. User is redirected back to your `redirect_uri` with an encrypted `inbox_token` + 4. Use the token to list/fetch CAS files from their inbox (`/v4/inbox/cas`) + 5. Files are uploaded to temporary cloud storage (URLs expire in 24 hours) + + **Security:** + - Read-only access (we cannot send emails) + - Tokens are encrypted with server-side secret + - User can revoke access anytime via `/v4/inbox/disconnect` + """ + @cached_property def with_raw_response(self) -> AsyncInboxResourceWithRawResponse: """ diff --git a/src/cas_parser/resources/kfintech.py b/src/cas_parser/resources/kfintech.py index 32077c5..358f415 100644 --- a/src/cas_parser/resources/kfintech.py +++ b/src/cas_parser/resources/kfintech.py @@ -22,6 +22,8 @@ class KfintechResource(SyncAPIResource): + """Endpoints for generating new CAS documents via email mailback (KFintech).""" + @cached_property def with_raw_response(self) -> KfintechResourceWithRawResponse: """ @@ -103,6 +105,8 @@ def generate_cas( class AsyncKfintechResource(AsyncAPIResource): + """Endpoints for generating new CAS documents via email mailback (KFintech).""" + @cached_property def with_raw_response(self) -> AsyncKfintechResourceWithRawResponse: """ diff --git a/src/cas_parser/resources/logs.py b/src/cas_parser/resources/logs.py index 45ba056..625baee 100644 --- a/src/cas_parser/resources/logs.py +++ b/src/cas_parser/resources/logs.py @@ -26,6 +26,11 @@ class LogsResource(SyncAPIResource): + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ + @cached_property def with_raw_response(self) -> LogsResourceWithRawResponse: """ @@ -147,6 +152,11 @@ def get_summary( class AsyncLogsResource(AsyncAPIResource): + """ + Endpoints for checking API quota and credits usage. + These endpoints help you monitor your API usage and remaining quota. + """ + @cached_property def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: """ diff --git a/src/cas_parser/resources/nsdl.py b/src/cas_parser/resources/nsdl.py index 4911e54..4312757 100644 --- a/src/cas_parser/resources/nsdl.py +++ b/src/cas_parser/resources/nsdl.py @@ -24,6 +24,8 @@ class NsdlResource(SyncAPIResource): + """Endpoints for parsing CAS PDF files from different sources.""" + @cached_property def with_raw_response(self) -> NsdlResourceWithRawResponse: """ @@ -101,6 +103,8 @@ def parse( class AsyncNsdlResource(AsyncAPIResource): + """Endpoints for parsing CAS PDF files from different sources.""" + @cached_property def with_raw_response(self) -> AsyncNsdlResourceWithRawResponse: """ diff --git a/src/cas_parser/resources/smart.py b/src/cas_parser/resources/smart.py index 41a4b6f..0d85213 100644 --- a/src/cas_parser/resources/smart.py +++ b/src/cas_parser/resources/smart.py @@ -24,6 +24,8 @@ class SmartResource(SyncAPIResource): + """Endpoints for parsing CAS PDF files from different sources.""" + @cached_property def with_raw_response(self) -> SmartResourceWithRawResponse: """ @@ -102,6 +104,8 @@ def parse_cas_pdf( class AsyncSmartResource(AsyncAPIResource): + """Endpoints for parsing CAS PDF files from different sources.""" + @cached_property def with_raw_response(self) -> AsyncSmartResourceWithRawResponse: """ diff --git a/src/cas_parser/resources/verify_token.py b/src/cas_parser/resources/verify_token.py index 06981d4..2247e4c 100644 --- a/src/cas_parser/resources/verify_token.py +++ b/src/cas_parser/resources/verify_token.py @@ -20,6 +20,12 @@ class VerifyTokenResource(SyncAPIResource): + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ + @cached_property def with_raw_response(self) -> VerifyTokenResourceWithRawResponse: """ @@ -64,6 +70,12 @@ def verify( class AsyncVerifyTokenResource(AsyncAPIResource): + """ + Endpoints for managing access tokens for the Portfolio Connect SDK. + Use these to generate short-lived `at_` prefixed tokens that can be safely passed to frontend applications. + Access tokens can be used in place of API keys on all v4 endpoints. + """ + @cached_property def with_raw_response(self) -> AsyncVerifyTokenResourceWithRawResponse: """ From 69dc430d9160343df9714af89921de50f08adf07 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:14:16 +0000 Subject: [PATCH 082/116] chore(internal): refactor authentication internals --- src/cas_parser/_base_client.py | 34 ++++++++++++++++++++++++++++------ src/cas_parser/_client.py | 19 +++++++++++++++---- src/cas_parser/_models.py | 6 ++++++ src/cas_parser/_types.py | 3 ++- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/cas_parser/_base_client.py b/src/cas_parser/_base_client.py index 9937027..5b130cf 100644 --- a/src/cas_parser/_base_client.py +++ b/src/cas_parser/_base_client.py @@ -63,7 +63,7 @@ ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping from ._compat import PYDANTIC_V1, model_copy, model_dump -from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type +from ._models import GenericModel, SecurityOptions, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, BaseAPIResponse, @@ -432,9 +432,27 @@ def _make_status_error( ) -> _exceptions.APIStatusError: raise NotImplementedError() + def _auth_headers( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> dict[str, str]: + return {} + + def _auth_query( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> dict[str, str]: + return {} + + def _custom_auth( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> httpx.Auth | None: + return None + def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: custom_headers = options.headers or {} - headers_dict = _merge_mappings(self.default_headers, custom_headers) + headers_dict = _merge_mappings({**self._auth_headers(options.security), **self.default_headers}, custom_headers) self._validate_headers(headers_dict, custom_headers) # headers are case-insensitive while dictionaries are not. @@ -506,7 +524,7 @@ def _build_request( raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") headers = self._build_headers(options, retries_taken=retries_taken) - params = _merge_mappings(self.default_query, options.params) + params = _merge_mappings({**self._auth_query(options.security), **self.default_query}, options.params) content_type = headers.get("Content-Type") files = options.files @@ -671,7 +689,6 @@ def default_headers(self) -> dict[str, str | Omit]: "Content-Type": "application/json", "User-Agent": self.user_agent, **self.platform_headers(), - **self.auth_headers, **self._custom_headers, } @@ -990,8 +1007,9 @@ def request( self._prepare_request(request) kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth + custom_auth = self._custom_auth(options.security) + if custom_auth is not None: + kwargs["auth"] = custom_auth if options.follow_redirects is not None: kwargs["follow_redirects"] = options.follow_redirects @@ -1952,6 +1970,7 @@ def make_request_options( idempotency_key: str | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, post_parser: PostParser | NotGiven = not_given, + security: SecurityOptions | None = None, ) -> RequestOptions: """Create a dict of type RequestOptions without keys of NotGiven values.""" options: RequestOptions = {} @@ -1977,6 +1996,9 @@ def make_request_options( # internal options["post_parser"] = post_parser # type: ignore + if security is not None: + options["security"] = security + return options diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py index bc29c05..1f11392 100644 --- a/src/cas_parser/_client.py +++ b/src/cas_parser/_client.py @@ -21,6 +21,7 @@ ) from ._utils import is_given, get_async_library from ._compat import cached_property +from ._models import SecurityOptions from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, CasParserError @@ -274,9 +275,14 @@ def with_streaming_response(self) -> CasParserWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="comma") - @property @override - def auth_headers(self) -> dict[str, str]: + def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: + return { + **(self._api_key_auth if security.get("api_key_auth", False) else {}), + } + + @property + def _api_key_auth(self) -> dict[str, str]: api_key = self.api_key return {"x-api-key": api_key} @@ -578,9 +584,14 @@ def with_streaming_response(self) -> AsyncCasParserWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="comma") - @property @override - def auth_headers(self) -> dict[str, str]: + def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: + return { + **(self._api_key_auth if security.get("api_key_auth", False) else {}), + } + + @property + def _api_key_auth(self) -> dict[str, str]: api_key = self.api_key return {"x-api-key": api_key} diff --git a/src/cas_parser/_models.py b/src/cas_parser/_models.py index 29070e0..6df43eb 100644 --- a/src/cas_parser/_models.py +++ b/src/cas_parser/_models.py @@ -791,6 +791,10 @@ def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: return RootModel[type_] # type: ignore +class SecurityOptions(TypedDict, total=False): + api_key_auth: bool + + class FinalRequestOptionsInput(TypedDict, total=False): method: Required[str] url: Required[str] @@ -804,6 +808,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): json_data: Body extra_json: AnyMapping follow_redirects: bool + security: SecurityOptions @final @@ -818,6 +823,7 @@ class FinalRequestOptions(pydantic.BaseModel): idempotency_key: Union[str, None] = None post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + security: SecurityOptions = {"api_key_auth": True} content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override diff --git a/src/cas_parser/_types.py b/src/cas_parser/_types.py index 3f7802c..f3d52b8 100644 --- a/src/cas_parser/_types.py +++ b/src/cas_parser/_types.py @@ -36,7 +36,7 @@ from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport if TYPE_CHECKING: - from ._models import BaseModel + from ._models import BaseModel, SecurityOptions from ._response import APIResponse, AsyncAPIResponse Transport = BaseTransport @@ -121,6 +121,7 @@ class RequestOptions(TypedDict, total=False): extra_json: AnyMapping idempotency_key: str follow_redirects: bool + security: SecurityOptions # Sentinel class used until PEP 0661 is accepted From 2cd0b970dec7a5ea996c9ed4e4e0da9e0c093e05 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:23:02 +0000 Subject: [PATCH 083/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7deae33..59565e8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.6.0" + ".": "1.6.1" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 85106e5..1e47163 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.6.0" +version = "1.6.1" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 1d9ca9a..a11f5b5 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.6.0" # x-release-please-version +__version__ = "1.6.1" # x-release-please-version diff --git a/uv.lock b/uv.lock index 9339e2e..b9f8d37 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.6.0" +version = "1.6.1" source = { editable = "." } dependencies = [ { name = "anyio" }, From ddad9b64e555700bbd937f7a54d9a0b39f967153 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 17:22:10 +0000 Subject: [PATCH 084/116] chore(ci): skip uploading artifacts on stainless-internal branches --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b10752..c311a1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,14 +55,18 @@ jobs: run: uv build - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/cas-parser-python' + if: |- + github.repository == 'stainless-sdks/cas-parser-python' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/cas-parser-python' + if: |- + github.repository == 'stainless-sdks/cas-parser-python' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From d4ef3214c08b5fee50e6750af18f2873eb305921 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 09:54:36 +0000 Subject: [PATCH 085/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 59565e8..4af5ef4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.6.1" + ".": "1.6.2" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1e47163..e9c8f63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.6.1" +version = "1.6.2" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index a11f5b5..f4e6b40 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.6.1" # x-release-please-version +__version__ = "1.6.2" # x-release-please-version diff --git a/uv.lock b/uv.lock index b9f8d37..8ce8006 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.6.1" +version = "1.6.2" source = { editable = "." } dependencies = [ { name = "anyio" }, From c675a30b867538b5701e577c8a098675072349a5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:47:23 +0000 Subject: [PATCH 086/116] fix(pydantic): do not pass `by_alias` unless set --- src/cas_parser/_compat.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/cas_parser/_compat.py b/src/cas_parser/_compat.py index 786ff42..e6690a4 100644 --- a/src/cas_parser/_compat.py +++ b/src/cas_parser/_compat.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload from datetime import date, datetime -from typing_extensions import Self, Literal +from typing_extensions import Self, Literal, TypedDict import pydantic from pydantic.fields import FieldInfo @@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: return model.model_dump_json(indent=indent) +class _ModelDumpKwargs(TypedDict, total=False): + by_alias: bool + + def model_dump( model: pydantic.BaseModel, *, @@ -142,6 +146,9 @@ def model_dump( by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): + kwargs: _ModelDumpKwargs = {} + if by_alias is not None: + kwargs["by_alias"] = by_alias return model.model_dump( mode=mode, exclude=exclude, @@ -149,7 +156,7 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, - by_alias=by_alias, + **kwargs, ) return cast( "dict[str, Any]", From 6f80dcd3b42b31d09c80200e2e144a85d5242fbc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:54:28 +0000 Subject: [PATCH 087/116] fix(deps): bump minimum typing-extensions version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e9c8f63..985005e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", + "typing-extensions>=4.14, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", diff --git a/uv.lock b/uv.lock index 8ce8006..dde2b03 100644 --- a/uv.lock +++ b/uv.lock @@ -277,7 +277,7 @@ requires-dist = [ { name = "httpx-aiohttp", marker = "extra == 'aiohttp'", specifier = ">=0.1.9" }, { name = "pydantic", specifier = ">=1.9.0,<3" }, { name = "sniffio" }, - { name = "typing-extensions", specifier = ">=4.10,<5" }, + { name = "typing-extensions", specifier = ">=4.14,<5" }, ] provides-extras = ["aiohttp"] From 9135d645d6ceffa4f7d341a0fee37e9359c582b2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:00:01 +0000 Subject: [PATCH 088/116] chore(internal): tweak CI branches --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c311a1a..9b8828d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' From fe16a1cad24233dc6a94123f071a041db59f7118 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:22:27 +0000 Subject: [PATCH 089/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4af5ef4..70a9c76 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.6.2" + ".": "1.6.3" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 985005e..dcc280e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.6.2" +version = "1.6.3" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index f4e6b40..43dd9a6 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.6.2" # x-release-please-version +__version__ = "1.6.3" # x-release-please-version diff --git a/uv.lock b/uv.lock index dde2b03..5069e72 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.6.2" +version = "1.6.3" source = { editable = "." } dependencies = [ { name = "anyio" }, From 3a5d001af8065a427bde38f087e7401acbadfb3e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:27:24 +0000 Subject: [PATCH 090/116] fix: sanitize endpoint path params --- src/cas_parser/_utils/__init__.py | 1 + src/cas_parser/_utils/_path.py | 127 ++++++++++++++++++++++ src/cas_parser/resources/cdsl/fetch.py | 6 +- src/cas_parser/resources/inbound_email.py | 10 +- tests/test_utils/test_path.py | 89 +++++++++++++++ 5 files changed, 225 insertions(+), 8 deletions(-) create mode 100644 src/cas_parser/_utils/_path.py create mode 100644 tests/test_utils/test_path.py diff --git a/src/cas_parser/_utils/__init__.py b/src/cas_parser/_utils/__init__.py index dc64e29..10cb66d 100644 --- a/src/cas_parser/_utils/__init__.py +++ b/src/cas_parser/_utils/__init__.py @@ -1,3 +1,4 @@ +from ._path import path_template as path_template from ._sync import asyncify as asyncify from ._proxy import LazyProxy as LazyProxy from ._utils import ( diff --git a/src/cas_parser/_utils/_path.py b/src/cas_parser/_utils/_path.py new file mode 100644 index 0000000..4d6e1e4 --- /dev/null +++ b/src/cas_parser/_utils/_path.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import re +from typing import ( + Any, + Mapping, + Callable, +) +from urllib.parse import quote + +# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). +_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") + +_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") + + +def _quote_path_segment_part(value: str) -> str: + """Percent-encode `value` for use in a URI path segment. + + Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + """ + # quote() already treats unreserved characters (letters, digits, and -._~) + # as safe, so we only need to add sub-delims, ':', and '@'. + # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. + return quote(value, safe="!$&'()*+,;=:@") + + +def _quote_query_part(value: str) -> str: + """Percent-encode `value` for use in a URI query string. + + Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + """ + return quote(value, safe="!$'()*+,;:@/?") + + +def _quote_fragment_part(value: str) -> str: + """Percent-encode `value` for use in a URI fragment. + + Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + """ + return quote(value, safe="!$&'()*+,;=:@/?") + + +def _interpolate( + template: str, + values: Mapping[str, Any], + quoter: Callable[[str], str], +) -> str: + """Replace {name} placeholders in `template`, quoting each value with `quoter`. + + Placeholder names are looked up in `values`. + + Raises: + KeyError: If a placeholder is not found in `values`. + """ + # re.split with a capturing group returns alternating + # [text, name, text, name, ..., text] elements. + parts = _PLACEHOLDER_RE.split(template) + + for i in range(1, len(parts), 2): + name = parts[i] + if name not in values: + raise KeyError(f"a value for placeholder {{{name}}} was not provided") + val = values[name] + if val is None: + parts[i] = "null" + elif isinstance(val, bool): + parts[i] = "true" if val else "false" + else: + parts[i] = quoter(str(values[name])) + + return "".join(parts) + + +def path_template(template: str, /, **kwargs: Any) -> str: + """Interpolate {name} placeholders in `template` from keyword arguments. + + Args: + template: The template string containing {name} placeholders. + **kwargs: Keyword arguments to interpolate into the template. + + Returns: + The template with placeholders interpolated and percent-encoded. + + Safe characters for percent-encoding are dependent on the URI component. + Placeholders in path and fragment portions are percent-encoded where the `segment` + and `fragment` sets from RFC 3986 respectively are considered safe. + Placeholders in the query portion are percent-encoded where the `query` set from + RFC 3986 §3.3 is considered safe except for = and & characters. + + Raises: + KeyError: If a placeholder is not found in `kwargs`. + ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). + """ + # Split the template into path, query, and fragment portions. + fragment_template: str | None = None + query_template: str | None = None + + rest = template + if "#" in rest: + rest, fragment_template = rest.split("#", 1) + if "?" in rest: + rest, query_template = rest.split("?", 1) + path_template = rest + + # Interpolate each portion with the appropriate quoting rules. + path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) + + # Reject dot-segments (. and ..) in the final assembled path. The check + # runs after interpolation so that adjacent placeholders or a mix of static + # text and placeholders that together form a dot-segment are caught. + # Also reject percent-encoded dot-segments to protect against incorrectly + # implemented normalization in servers/proxies. + for segment in path_result.split("/"): + if _DOT_SEGMENT_RE.match(segment): + raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") + + result = path_result + if query_template is not None: + result += "?" + _interpolate(query_template, kwargs, _quote_query_part) + if fragment_template is not None: + result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) + + return result diff --git a/src/cas_parser/resources/cdsl/fetch.py b/src/cas_parser/resources/cdsl/fetch.py index 191c3a7..6198ad2 100644 --- a/src/cas_parser/resources/cdsl/fetch.py +++ b/src/cas_parser/resources/cdsl/fetch.py @@ -5,7 +5,7 @@ import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -138,7 +138,7 @@ def verify_otp( if not session_id: raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") return self._post( - f"/v4/cdsl/fetch/{session_id}/verify", + path_template("/v4/cdsl/fetch/{session_id}/verify", session_id=session_id), body=maybe_transform( { "otp": otp, @@ -269,7 +269,7 @@ async def verify_otp( if not session_id: raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") return await self._post( - f"/v4/cdsl/fetch/{session_id}/verify", + path_template("/v4/cdsl/fetch/{session_id}/verify", session_id=session_id), body=await async_maybe_transform( { "otp": otp, diff --git a/src/cas_parser/resources/inbound_email.py b/src/cas_parser/resources/inbound_email.py index 3e45f5d..adc1c15 100644 --- a/src/cas_parser/resources/inbound_email.py +++ b/src/cas_parser/resources/inbound_email.py @@ -9,7 +9,7 @@ from ..types import inbound_email_list_params, inbound_email_create_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -185,7 +185,7 @@ def retrieve( if not inbound_email_id: raise ValueError(f"Expected a non-empty value for `inbound_email_id` but received {inbound_email_id!r}") return self._get( - f"/v4/inbound-email/{inbound_email_id}", + path_template("/v4/inbound-email/{inbound_email_id}", inbound_email_id=inbound_email_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -274,7 +274,7 @@ def delete( if not inbound_email_id: raise ValueError(f"Expected a non-empty value for `inbound_email_id` but received {inbound_email_id!r}") return self._delete( - f"/v4/inbound-email/{inbound_email_id}", + path_template("/v4/inbound-email/{inbound_email_id}", inbound_email_id=inbound_email_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -440,7 +440,7 @@ async def retrieve( if not inbound_email_id: raise ValueError(f"Expected a non-empty value for `inbound_email_id` but received {inbound_email_id!r}") return await self._get( - f"/v4/inbound-email/{inbound_email_id}", + path_template("/v4/inbound-email/{inbound_email_id}", inbound_email_id=inbound_email_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -529,7 +529,7 @@ async def delete( if not inbound_email_id: raise ValueError(f"Expected a non-empty value for `inbound_email_id` but received {inbound_email_id!r}") return await self._delete( - f"/v4/inbound-email/{inbound_email_id}", + path_template("/v4/inbound-email/{inbound_email_id}", inbound_email_id=inbound_email_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py new file mode 100644 index 0000000..cf039c7 --- /dev/null +++ b/tests/test_utils/test_path.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from cas_parser._utils._path import path_template + + +@pytest.mark.parametrize( + "template, kwargs, expected", + [ + ("/v1/{id}", dict(id="abc"), "/v1/abc"), + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), + ("/v1/static", {}, "/v1/static"), + ("", {}, ""), + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), + ("/v1/{v}", dict(v=None), "/v1/null"), + ("/v1/{v}", dict(v=True), "/v1/true"), + ("/v1/{v}", dict(v=False), "/v1/false"), + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine + ( + "/v1/{a}?query={b}", + dict(a="../../other/endpoint", b="a&bad=true"), + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", + ), + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input + # Query: slash and ? are safe, # is not + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), + # Fragment: slash and ? are safe + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), + # Path: slash, ? and # are all encoded + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), + # same var encoded differently by component + ( + "/v1/{v}?q={v}#{v}", + dict(v="a/b?c#d"), + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", + ), + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection + ], +) +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: + assert path_template(template, **kwargs) == expected + + +def test_missing_kwarg_raises_key_error() -> None: + with pytest.raises(KeyError, match="org_id"): + path_template("/v1/{org_id}") + + +@pytest.mark.parametrize( + "template, kwargs", + [ + ("{a}/path", dict(a=".")), + ("{a}/path", dict(a="..")), + ("/v1/{a}", dict(a=".")), + ("/v1/{a}", dict(a="..")), + ("/v1/{a}/path", dict(a=".")), + ("/v1/{a}/path", dict(a="..")), + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." + ("/v1/{a}.", dict(a=".")), # var + static → ".." + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/{v}?q=1", dict(v="..")), + ("/v1/{v}#frag", dict(v="..")), + ], +) +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="dot-segment"): + path_template(template, **kwargs) From 8e132192bfa809f10dafee98f00331061ef3a4ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 04:30:31 +0000 Subject: [PATCH 091/116] chore(internal): update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 95ceb18..3824f4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log _dev __pycache__ From c78a153a1415d360b360c16748530f52b186c559 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 03:09:25 +0000 Subject: [PATCH 092/116] chore(ci): skip lint on metadata-only changes Note that we still want to run tests, as these depend on the metadata. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b8828d..7923085 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 @@ -35,7 +35,7 @@ jobs: run: ./scripts/lint build: - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') timeout-minutes: 10 name: build permissions: From 1eafd5e592e344fca0b26a99508f31404e037a2e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:45:14 +0000 Subject: [PATCH 093/116] feat(internal): implement indices array format for query and form serialization --- src/cas_parser/_qs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cas_parser/_qs.py b/src/cas_parser/_qs.py index ada6fd3..de8c99b 100644 --- a/src/cas_parser/_qs.py +++ b/src/cas_parser/_qs.py @@ -101,7 +101,10 @@ def _stringify_item( items.extend(self._stringify_item(key, item, opts)) return items elif array_format == "indices": - raise NotImplementedError("The array indices format is not supported yet") + items = [] + for i, item in enumerate(value): + items.extend(self._stringify_item(f"{key}[{i}]", item, opts)) + return items elif array_format == "brackets": items = [] key = key + "[]" From 93461b963ce7830d8cfa8f79aa29b7426d738dab Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:56:39 +0000 Subject: [PATCH 094/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 70a9c76..cce9d1c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.6.3" + ".": "1.7.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index dcc280e..724085d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.6.3" +version = "1.7.0" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 43dd9a6..e314464 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.6.3" # x-release-please-version +__version__ = "1.7.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index 5069e72..3de2c14 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.6.3" +version = "1.7.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From 2e812a1601cbf5cf63fcbbd8a065332e23da8f9b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:23:18 +0000 Subject: [PATCH 095/116] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 603f57a..25ece8f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-d9763d006969b49a1473851069fdfa429eb13133b64103a62963bb70ddb22305.yml -openapi_spec_hash: 6aee689b7a759b12c85c088c15e29bc0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-d868ff00b7b07f6b6802b00f22fad531a91a76bb219a634f3f90fe488bd499ba.yml +openapi_spec_hash: 20e9f2fc31feee78878cdf56e46dab60 config_hash: 5509bb7a961ae2e79114b24c381606d4 From 50dafe461d0754b5aa7ffe5d7e3d02d279eecb14 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 04:38:32 +0000 Subject: [PATCH 096/116] fix(client): preserve hardcoded query params when merging with user params --- src/cas_parser/_base_client.py | 4 +++ tests/test_client.py | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/cas_parser/_base_client.py b/src/cas_parser/_base_client.py index 5b130cf..33ae5fa 100644 --- a/src/cas_parser/_base_client.py +++ b/src/cas_parser/_base_client.py @@ -558,6 +558,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} diff --git a/tests/test_client.py b/tests/test_client.py index 9e2377f..dd96b67 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -429,6 +429,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: CasParser) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: CasParser) -> None: request = client._build_request( FinalRequestOptions( @@ -1320,6 +1344,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncCasParser) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: CasParser) -> None: request = client._build_request( FinalRequestOptions( From b0f77fff945e4197d3194a4f32884669b363a25d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 07:46:54 +0000 Subject: [PATCH 097/116] fix: ensure file data are only sent as 1 parameter --- src/cas_parser/_utils/_utils.py | 5 +++-- tests/test_extract_files.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/cas_parser/_utils/_utils.py b/src/cas_parser/_utils/_utils.py index eec7f4a..63b8cd6 100644 --- a/src/cas_parser/_utils/_utils.py +++ b/src/cas_parser/_utils/_utils.py @@ -86,8 +86,9 @@ def _extract_items( index += 1 if is_dict(obj): try: - # We are at the last entry in the path so we must remove the field - if (len(path)) == index: + # Remove the field if there are no more dict keys in the path, + # only "" traversal markers or end. + if all(p == "" for p in path[index:]): item = obj.pop(key) else: item = obj[key] diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 37985fb..bb4d8bb 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -35,6 +35,15 @@ def test_multiple_files() -> None: assert query == {"documents": [{}, {}]} +def test_top_level_file_array() -> None: + query = {"files": [b"file one", b"file two"], "title": "hello"} + assert extract_files(query, paths=[["files", ""]]) == [ + ("files[]", b"file one"), + ("files[]", b"file two"), + ] + assert query == {"title": "hello"} + + @pytest.mark.parametrize( "query,paths,expected", [ From 1070667e02d78a36b17c4a02b7adb7e51ad87395 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 07:14:00 +0000 Subject: [PATCH 098/116] perf(client): optimize file structure copying in multipart requests --- src/cas_parser/_files.py | 56 ++++++++++++- src/cas_parser/_utils/__init__.py | 1 - src/cas_parser/_utils/_utils.py | 15 ---- src/cas_parser/resources/cams_kfintech.py | 13 +-- src/cas_parser/resources/cdsl/cdsl.py | 13 +-- src/cas_parser/resources/contract_note.py | 13 +-- src/cas_parser/resources/nsdl.py | 13 +-- src/cas_parser/resources/smart.py | 13 +-- tests/test_deepcopy.py | 58 ------------- tests/test_files.py | 99 ++++++++++++++++++++++- 10 files changed, 191 insertions(+), 103 deletions(-) delete mode 100644 tests/test_deepcopy.py diff --git a/src/cas_parser/_files.py b/src/cas_parser/_files.py index cc14c14..0fdce17 100644 --- a/src/cas_parser/_files.py +++ b/src/cas_parser/_files.py @@ -3,8 +3,8 @@ import io import os import pathlib -from typing import overload -from typing_extensions import TypeGuard +from typing import Sequence, cast, overload +from typing_extensions import TypeVar, TypeGuard import anyio @@ -17,7 +17,9 @@ HttpxFileContent, HttpxRequestFiles, ) -from ._utils import is_tuple_t, is_mapping_t, is_sequence_t +from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t + +_T = TypeVar("_T") def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: @@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent: return await anyio.Path(file).read_bytes() return file + + +def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: + """Copy only the containers along the given paths. + + Used to guard against mutation by extract_files without copying the entire structure. + Only dicts and lists that lie on a path are copied; everything else + is returned by reference. + + For example, given paths=[["foo", "files", "file"]] and the structure: + { + "foo": { + "bar": {"baz": {}}, + "files": {"file": } + } + } + The root dict, "foo", and "files" are copied (they lie on the path). + "bar" and "baz" are returned by reference (off the path). + """ + return _deepcopy_with_paths(item, paths, 0) + + +def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: + if not paths: + return item + if is_mapping(item): + key_to_paths: dict[str, list[Sequence[str]]] = {} + for path in paths: + if index < len(path): + key_to_paths.setdefault(path[index], []).append(path) + + # if no path continues through this mapping, it won't be mutated and copying it is redundant + if not key_to_paths: + return item + + result = dict(item) + for key, subpaths in key_to_paths.items(): + if key in result: + result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) + return cast(_T, result) + if is_list(item): + array_paths = [path for path in paths if index < len(path) and path[index] == ""] + + # if no path expects a list here, nothing will be mutated inside it - return by reference + if not array_paths: + return cast(_T, item) + return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) + return item diff --git a/src/cas_parser/_utils/__init__.py b/src/cas_parser/_utils/__init__.py index 10cb66d..1c090e5 100644 --- a/src/cas_parser/_utils/__init__.py +++ b/src/cas_parser/_utils/__init__.py @@ -24,7 +24,6 @@ coerce_integer as coerce_integer, file_from_path as file_from_path, strip_not_given as strip_not_given, - deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, maybe_coerce_float as maybe_coerce_float, get_required_header as get_required_header, diff --git a/src/cas_parser/_utils/_utils.py b/src/cas_parser/_utils/_utils.py index 63b8cd6..771859f 100644 --- a/src/cas_parser/_utils/_utils.py +++ b/src/cas_parser/_utils/_utils.py @@ -177,21 +177,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: return isinstance(obj, Iterable) -def deepcopy_minimal(item: _T) -> _T: - """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: - - - mappings, e.g. `dict` - - list - - This is done for performance reasons. - """ - if is_mapping(item): - return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) - if is_list(item): - return cast(_T, [deepcopy_minimal(entry) for entry in item]) - return item - - # copied from https://github.com/Rapptz/RoboDanny def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: size = len(seq) diff --git a/src/cas_parser/resources/cams_kfintech.py b/src/cas_parser/resources/cams_kfintech.py index fb32699..c11c3a9 100644 --- a/src/cas_parser/resources/cams_kfintech.py +++ b/src/cas_parser/resources/cams_kfintech.py @@ -7,8 +7,9 @@ import httpx from ..types import cams_kfintech_parse_params +from .._files import deepcopy_with_paths from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -78,12 +79,13 @@ def parse( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "password": password, "pdf_file": pdf_file, "pdf_url": pdf_url, - } + }, + [["pdf_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) if files: @@ -157,12 +159,13 @@ async def parse( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "password": password, "pdf_file": pdf_file, "pdf_url": pdf_url, - } + }, + [["pdf_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) if files: diff --git a/src/cas_parser/resources/cdsl/cdsl.py b/src/cas_parser/resources/cdsl/cdsl.py index d0c69b9..75e66b3 100644 --- a/src/cas_parser/resources/cdsl/cdsl.py +++ b/src/cas_parser/resources/cdsl/cdsl.py @@ -15,8 +15,9 @@ AsyncFetchResourceWithStreamingResponse, ) from ...types import cdsl_parse_pdf_params +from ..._files import deepcopy_with_paths from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from ..._utils import extract_files, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -94,12 +95,13 @@ def parse_pdf( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "password": password, "pdf_file": pdf_file, "pdf_url": pdf_url, - } + }, + [["pdf_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) if files: @@ -181,12 +183,13 @@ async def parse_pdf( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "password": password, "pdf_file": pdf_file, "pdf_url": pdf_url, - } + }, + [["pdf_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) if files: diff --git a/src/cas_parser/resources/contract_note.py b/src/cas_parser/resources/contract_note.py index 7133be7..45e6464 100644 --- a/src/cas_parser/resources/contract_note.py +++ b/src/cas_parser/resources/contract_note.py @@ -8,8 +8,9 @@ import httpx from ..types import contract_note_parse_params +from .._files import deepcopy_with_paths from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -110,13 +111,14 @@ def parse( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "broker_type": broker_type, "password": password, "pdf_file": pdf_file, "pdf_url": pdf_url, - } + }, + [["pdf_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) if files: @@ -221,13 +223,14 @@ async def parse( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "broker_type": broker_type, "password": password, "pdf_file": pdf_file, "pdf_url": pdf_url, - } + }, + [["pdf_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) if files: diff --git a/src/cas_parser/resources/nsdl.py b/src/cas_parser/resources/nsdl.py index 4312757..9e3f8d1 100644 --- a/src/cas_parser/resources/nsdl.py +++ b/src/cas_parser/resources/nsdl.py @@ -7,8 +7,9 @@ import httpx from ..types import nsdl_parse_params +from .._files import deepcopy_with_paths from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -78,12 +79,13 @@ def parse( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "password": password, "pdf_file": pdf_file, "pdf_url": pdf_url, - } + }, + [["pdf_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) if files: @@ -157,12 +159,13 @@ async def parse( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "password": password, "pdf_file": pdf_file, "pdf_url": pdf_url, - } + }, + [["pdf_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) if files: diff --git a/src/cas_parser/resources/smart.py b/src/cas_parser/resources/smart.py index 0d85213..1cb95d5 100644 --- a/src/cas_parser/resources/smart.py +++ b/src/cas_parser/resources/smart.py @@ -7,8 +7,9 @@ import httpx from ..types import smart_parse_cas_pdf_params +from .._files import deepcopy_with_paths from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -79,12 +80,13 @@ def parse_cas_pdf( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "password": password, "pdf_file": pdf_file, "pdf_url": pdf_url, - } + }, + [["pdf_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) if files: @@ -159,12 +161,13 @@ async def parse_cas_pdf( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "password": password, "pdf_file": pdf_file, "pdf_url": pdf_url, - } + }, + [["pdf_file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]]) if files: diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index c1e03c0..0000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from cas_parser._utils import deepcopy_minimal - - -def assert_different_identities(obj1: object, obj2: object) -> None: - assert obj1 == obj2 - assert id(obj1) != id(obj2) - - -def test_simple_dict() -> None: - obj1 = {"foo": "bar"} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_dict() -> None: - obj1 = {"foo": {"bar": True}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - - -def test_complex_nested_dict() -> None: - obj1 = {"foo": {"bar": [{"hello": "world"}]}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) - assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) - - -def test_simple_list() -> None: - obj1 = ["a", "b", "c"] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_list() -> None: - obj1 = ["a", [1, 2, 3]] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1[1], obj2[1]) - - -class MyObject: ... - - -def test_ignores_other_types() -> None: - # custom classes - my_obj = MyObject() - obj1 = {"foo": my_obj} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert obj1["foo"] is my_obj - - # tuples - obj3 = ("a", "b") - obj4 = deepcopy_minimal(obj3) - assert obj3 is obj4 diff --git a/tests/test_files.py b/tests/test_files.py index 1f448b8..117534b 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,8 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from cas_parser._files import to_httpx_files, async_to_httpx_files +from cas_parser._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from cas_parser._utils import extract_files readme_path = Path(__file__).parent.parent.joinpath("README.md") @@ -49,3 +50,99 @@ def test_string_not_allowed() -> None: "file": "foo", # type: ignore } ) + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert obj1 is not obj2 + + +class TestDeepcopyWithPaths: + def test_copies_top_level_dict(self) -> None: + original = {"file": b"data", "other": "value"} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + + def test_file_value_is_same_reference(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + assert result["file"] is file_bytes + + def test_list_popped_wholesale(self) -> None: + files = [b"f1", b"f2"] + original = {"files": files, "title": "t"} + result = deepcopy_with_paths(original, [["files", ""]]) + assert_different_identities(result, original) + result_files = result["files"] + assert isinstance(result_files, list) + assert_different_identities(result_files, files) + + def test_nested_array_path_copies_list_and_elements(self) -> None: + elem1 = {"file": b"f1", "extra": 1} + elem2 = {"file": b"f2", "extra": 2} + original = {"items": [elem1, elem2]} + result = deepcopy_with_paths(original, [["items", "", "file"]]) + assert_different_identities(result, original) + result_items = result["items"] + assert isinstance(result_items, list) + assert_different_identities(result_items, original["items"]) + assert_different_identities(result_items[0], elem1) + assert_different_identities(result_items[1], elem2) + + def test_empty_paths_returns_same_object(self) -> None: + original = {"foo": "bar"} + result = deepcopy_with_paths(original, []) + assert result is original + + def test_multiple_paths(self) -> None: + f1 = b"file1" + f2 = b"file2" + original = {"a": f1, "b": f2, "c": "unchanged"} + result = deepcopy_with_paths(original, [["a"], ["b"]]) + assert_different_identities(result, original) + assert result["a"] is f1 + assert result["b"] is f2 + assert result["c"] is original["c"] + + def test_extract_files_does_not_mutate_original_top_level(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes, "other": "value"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_bytes)] + assert original == {"file": file_bytes, "other": "value"} + assert copied == {"other": "value"} + + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: + file1 = b"f1" + file2 = b"f2" + original = { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + + copied = deepcopy_with_paths(original, [["items", "", "file"]]) + extracted = extract_files(copied, paths=[["items", "", "file"]]) + + assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert original == { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + assert copied == { + "items": [ + {"extra": 1}, + {"extra": 2}, + ], + "title": "example", + } From b89155e437e8746406607dacfee46745c45216b7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:25:11 +0000 Subject: [PATCH 099/116] feat(api): api update --- .stats.yml | 4 +- src/cas_parser/resources/inbound_email.py | 92 +++++++------------ .../types/inbound_email_create_params.py | 26 +++--- .../types/inbound_email_create_response.py | 6 +- .../types/inbound_email_list_response.py | 6 +- .../types/inbound_email_retrieve_response.py | 6 +- tests/api_resources/test_inbound_email.py | 28 ++---- 7 files changed, 68 insertions(+), 100 deletions(-) diff --git a/.stats.yml b/.stats.yml index 25ece8f..9c6cf93 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-d868ff00b7b07f6b6802b00f22fad531a91a76bb219a634f3f90fe488bd499ba.yml -openapi_spec_hash: 20e9f2fc31feee78878cdf56e46dab60 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-78ef474b9e171a3eaa430a9dacdc2fa5c7f7d5f89147cb20573a355d3dbb9f0e.yml +openapi_spec_hash: 11b6e43ef4ed724f9804c9d790a4faee config_hash: 5509bb7a961ae2e79114b24c381606d4 diff --git a/src/cas_parser/resources/inbound_email.py b/src/cas_parser/resources/inbound_email.py index adc1c15..6028cba 100644 --- a/src/cas_parser/resources/inbound_email.py +++ b/src/cas_parser/resources/inbound_email.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, List +from typing import Dict, List, Optional from typing_extensions import Literal import httpx @@ -73,9 +73,9 @@ def with_streaming_response(self) -> InboundEmailResourceWithStreamingResponse: def create( self, *, - callback_url: str, alias: str | Omit = omit, allowed_sources: List[Literal["cdsl", "nsdl", "cams", "kfintech"]] | Omit = omit, + callback_url: Optional[str] | Omit = omit, metadata: Dict[str, str] | Omit = omit, reference: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -87,38 +87,19 @@ def create( ) -> InboundEmailCreateResponse: """ Create a dedicated inbound email address for collecting CAS statements via email - forwarding. + forwarding. When an investor forwards a CAS email to this address, we verify the + sender and make the file available to you. - **How it works:** + `callback_url` is **optional**: - 1. Create an inbound email with your webhook URL - 2. Display the email address to your user (e.g., "Forward your CAS to - ie_xxx@import.casparser.in") - 3. When an investor forwards a CAS email, we verify the sender and deliver to - your webhook - - **Webhook Delivery:** - - - We POST to your `callback_url` with JSON body containing files (matching - EmailCASFile schema) - - Failed deliveries are retried automatically with exponential backoff - - **Inactivity:** - - - Inbound emails with no activity in 30 days are marked inactive - - Active inbound emails remain operational indefinitely + - **Set it** — we POST each parsed email to your webhook as it arrives. + - **Omit it** — retrieve files via `GET /v4/inbound-email/{id}/files` without + building a webhook consumer. Args: - callback_url: Webhook URL where we POST email notifications. Must be HTTPS in production (HTTP - allowed for localhost during development). - - alias: Optional custom email prefix for user-friendly addresses. - - - Must be 3-32 characters - - Alphanumeric + hyphens only - - Must start and end with letter/number - - Example: `john-portfolio@import.casparser.in` - - If omitted, generates random ID like `ie_abc123xyz@import.casparser.in` + alias: Optional custom email prefix (e.g. `john-portfolio@import.casparser.in`). 3-32 + chars, alphanumeric + hyphens, must start/end with a letter or number. If + omitted, a random ID is generated. allowed_sources: Filter emails by CAS provider. If omitted, accepts all providers. @@ -127,6 +108,10 @@ def create( - `cams` → donotreply@camsonline.com - `kfintech` → samfS@kfintech.com + callback_url: Optional webhook URL where we POST parsed emails. Must be HTTPS in production + (HTTP allowed for localhost). If omitted, retrieve files via + `GET /v4/inbound-email/{id}/files`. + metadata: Optional key-value pairs (max 10) to include in webhook payload. Useful for passing context like plan_type, campaign_id, etc. @@ -145,9 +130,9 @@ def create( "/v4/inbound-email", body=maybe_transform( { - "callback_url": callback_url, "alias": alias, "allowed_sources": allowed_sources, + "callback_url": callback_url, "metadata": metadata, "reference": reference, }, @@ -328,9 +313,9 @@ def with_streaming_response(self) -> AsyncInboundEmailResourceWithStreamingRespo async def create( self, *, - callback_url: str, alias: str | Omit = omit, allowed_sources: List[Literal["cdsl", "nsdl", "cams", "kfintech"]] | Omit = omit, + callback_url: Optional[str] | Omit = omit, metadata: Dict[str, str] | Omit = omit, reference: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -342,38 +327,19 @@ async def create( ) -> InboundEmailCreateResponse: """ Create a dedicated inbound email address for collecting CAS statements via email - forwarding. + forwarding. When an investor forwards a CAS email to this address, we verify the + sender and make the file available to you. - **How it works:** + `callback_url` is **optional**: - 1. Create an inbound email with your webhook URL - 2. Display the email address to your user (e.g., "Forward your CAS to - ie_xxx@import.casparser.in") - 3. When an investor forwards a CAS email, we verify the sender and deliver to - your webhook - - **Webhook Delivery:** - - - We POST to your `callback_url` with JSON body containing files (matching - EmailCASFile schema) - - Failed deliveries are retried automatically with exponential backoff - - **Inactivity:** - - - Inbound emails with no activity in 30 days are marked inactive - - Active inbound emails remain operational indefinitely + - **Set it** — we POST each parsed email to your webhook as it arrives. + - **Omit it** — retrieve files via `GET /v4/inbound-email/{id}/files` without + building a webhook consumer. Args: - callback_url: Webhook URL where we POST email notifications. Must be HTTPS in production (HTTP - allowed for localhost during development). - - alias: Optional custom email prefix for user-friendly addresses. - - - Must be 3-32 characters - - Alphanumeric + hyphens only - - Must start and end with letter/number - - Example: `john-portfolio@import.casparser.in` - - If omitted, generates random ID like `ie_abc123xyz@import.casparser.in` + alias: Optional custom email prefix (e.g. `john-portfolio@import.casparser.in`). 3-32 + chars, alphanumeric + hyphens, must start/end with a letter or number. If + omitted, a random ID is generated. allowed_sources: Filter emails by CAS provider. If omitted, accepts all providers. @@ -382,6 +348,10 @@ async def create( - `cams` → donotreply@camsonline.com - `kfintech` → samfS@kfintech.com + callback_url: Optional webhook URL where we POST parsed emails. Must be HTTPS in production + (HTTP allowed for localhost). If omitted, retrieve files via + `GET /v4/inbound-email/{id}/files`. + metadata: Optional key-value pairs (max 10) to include in webhook payload. Useful for passing context like plan_type, campaign_id, etc. @@ -400,9 +370,9 @@ async def create( "/v4/inbound-email", body=await async_maybe_transform( { - "callback_url": callback_url, "alias": alias, "allowed_sources": allowed_sources, + "callback_url": callback_url, "metadata": metadata, "reference": reference, }, diff --git a/src/cas_parser/types/inbound_email_create_params.py b/src/cas_parser/types/inbound_email_create_params.py index 356d7e1..62ecc96 100644 --- a/src/cas_parser/types/inbound_email_create_params.py +++ b/src/cas_parser/types/inbound_email_create_params.py @@ -2,27 +2,18 @@ from __future__ import annotations -from typing import Dict, List -from typing_extensions import Literal, Required, TypedDict +from typing import Dict, List, Optional +from typing_extensions import Literal, TypedDict __all__ = ["InboundEmailCreateParams"] class InboundEmailCreateParams(TypedDict, total=False): - callback_url: Required[str] - """ - Webhook URL where we POST email notifications. Must be HTTPS in production (HTTP - allowed for localhost during development). - """ - alias: str - """Optional custom email prefix for user-friendly addresses. + """Optional custom email prefix (e.g. `john-portfolio@import.casparser.in`). - - Must be 3-32 characters - - Alphanumeric + hyphens only - - Must start and end with letter/number - - Example: `john-portfolio@import.casparser.in` - - If omitted, generates random ID like `ie_abc123xyz@import.casparser.in` + 3-32 chars, alphanumeric + hyphens, must start/end with a letter or number. If + omitted, a random ID is generated. """ allowed_sources: List[Literal["cdsl", "nsdl", "cams", "kfintech"]] @@ -34,6 +25,13 @@ class InboundEmailCreateParams(TypedDict, total=False): - `kfintech` → samfS@kfintech.com """ + callback_url: Optional[str] + """Optional webhook URL where we POST parsed emails. + + Must be HTTPS in production (HTTP allowed for localhost). If omitted, retrieve + files via `GET /v4/inbound-email/{id}/files`. + """ + metadata: Dict[str, str] """ Optional key-value pairs (max 10) to include in webhook payload. Useful for diff --git a/src/cas_parser/types/inbound_email_create_response.py b/src/cas_parser/types/inbound_email_create_response.py index 29f89dc..77611a6 100644 --- a/src/cas_parser/types/inbound_email_create_response.py +++ b/src/cas_parser/types/inbound_email_create_response.py @@ -16,7 +16,11 @@ class InboundEmailCreateResponse(BaseModel): """Accepted CAS providers (empty = all)""" callback_url: Optional[str] = None - """Webhook URL for email notifications""" + """Webhook URL for email notifications. + + `null` means files are only retrievable via `GET /v4/inbound-email/{id}/files` + (pull delivery). + """ created_at: Optional[datetime] = None """When the mailbox was created""" diff --git a/src/cas_parser/types/inbound_email_list_response.py b/src/cas_parser/types/inbound_email_list_response.py index b1eea1b..2d700b6 100644 --- a/src/cas_parser/types/inbound_email_list_response.py +++ b/src/cas_parser/types/inbound_email_list_response.py @@ -16,7 +16,11 @@ class InboundEmail(BaseModel): """Accepted CAS providers (empty = all)""" callback_url: Optional[str] = None - """Webhook URL for email notifications""" + """Webhook URL for email notifications. + + `null` means files are only retrievable via `GET /v4/inbound-email/{id}/files` + (pull delivery). + """ created_at: Optional[datetime] = None """When the mailbox was created""" diff --git a/src/cas_parser/types/inbound_email_retrieve_response.py b/src/cas_parser/types/inbound_email_retrieve_response.py index 601fc87..cfcb7e5 100644 --- a/src/cas_parser/types/inbound_email_retrieve_response.py +++ b/src/cas_parser/types/inbound_email_retrieve_response.py @@ -16,7 +16,11 @@ class InboundEmailRetrieveResponse(BaseModel): """Accepted CAS providers (empty = all)""" callback_url: Optional[str] = None - """Webhook URL for email notifications""" + """Webhook URL for email notifications. + + `null` means files are only retrievable via `GET /v4/inbound-email/{id}/files` + (pull delivery). + """ created_at: Optional[datetime] = None """When the mailbox was created""" diff --git a/tests/api_resources/test_inbound_email.py b/tests/api_resources/test_inbound_email.py index 6970229..ca9e782 100644 --- a/tests/api_resources/test_inbound_email.py +++ b/tests/api_resources/test_inbound_email.py @@ -25,18 +25,16 @@ class TestInboundEmail: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: CasParser) -> None: - inbound_email = client.inbound_email.create( - callback_url="https://api.yourapp.com/webhooks/cas-email", - ) + inbound_email = client.inbound_email.create() assert_matches_type(InboundEmailCreateResponse, inbound_email, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: CasParser) -> None: inbound_email = client.inbound_email.create( - callback_url="https://api.yourapp.com/webhooks/cas-email", alias="john-portfolio", allowed_sources=["cdsl", "nsdl"], + callback_url="https://api.yourapp.com/webhooks/cas-email", metadata={ "plan": "premium", "source": "onboarding", @@ -48,9 +46,7 @@ def test_method_create_with_all_params(self, client: CasParser) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: CasParser) -> None: - response = client.inbound_email.with_raw_response.create( - callback_url="https://api.yourapp.com/webhooks/cas-email", - ) + response = client.inbound_email.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -60,9 +56,7 @@ def test_raw_response_create(self, client: CasParser) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: CasParser) -> None: - with client.inbound_email.with_streaming_response.create( - callback_url="https://api.yourapp.com/webhooks/cas-email", - ) as response: + with client.inbound_email.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -202,18 +196,16 @@ class TestAsyncInboundEmail: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncCasParser) -> None: - inbound_email = await async_client.inbound_email.create( - callback_url="https://api.yourapp.com/webhooks/cas-email", - ) + inbound_email = await async_client.inbound_email.create() assert_matches_type(InboundEmailCreateResponse, inbound_email, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncCasParser) -> None: inbound_email = await async_client.inbound_email.create( - callback_url="https://api.yourapp.com/webhooks/cas-email", alias="john-portfolio", allowed_sources=["cdsl", "nsdl"], + callback_url="https://api.yourapp.com/webhooks/cas-email", metadata={ "plan": "premium", "source": "onboarding", @@ -225,9 +217,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncCasParser) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: - response = await async_client.inbound_email.with_raw_response.create( - callback_url="https://api.yourapp.com/webhooks/cas-email", - ) + response = await async_client.inbound_email.with_raw_response.create() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -237,9 +227,7 @@ async def test_raw_response_create(self, async_client: AsyncCasParser) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncCasParser) -> None: - async with async_client.inbound_email.with_streaming_response.create( - callback_url="https://api.yourapp.com/webhooks/cas-email", - ) as response: + async with async_client.inbound_email.with_streaming_response.create() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From d342121f4e97165dedfdd2611664302a3fdaf31c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 21:25:18 +0000 Subject: [PATCH 100/116] feat(api): api update --- .stats.yml | 4 ++-- tests/api_resources/test_inbound_email.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9c6cf93..e26d438 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-78ef474b9e171a3eaa430a9dacdc2fa5c7f7d5f89147cb20573a355d3dbb9f0e.yml -openapi_spec_hash: 11b6e43ef4ed724f9804c9d790a4faee +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-e5c0c65637cdf3a6c4360b8193973b73a3d35ad1056ef607c3319ef03e591a55.yml +openapi_spec_hash: 7515d1e5fe3130b9f5411f7aacbc8a64 config_hash: 5509bb7a961ae2e79114b24c381606d4 diff --git a/tests/api_resources/test_inbound_email.py b/tests/api_resources/test_inbound_email.py index ca9e782..9b120e5 100644 --- a/tests/api_resources/test_inbound_email.py +++ b/tests/api_resources/test_inbound_email.py @@ -69,7 +69,7 @@ def test_streaming_response_create(self, client: CasParser) -> None: @parametrize def test_method_retrieve(self, client: CasParser) -> None: inbound_email = client.inbound_email.retrieve( - "ie_a1b2c3d4e5f6", + "inbound_email_id", ) assert_matches_type(InboundEmailRetrieveResponse, inbound_email, path=["response"]) @@ -77,7 +77,7 @@ def test_method_retrieve(self, client: CasParser) -> None: @parametrize def test_raw_response_retrieve(self, client: CasParser) -> None: response = client.inbound_email.with_raw_response.retrieve( - "ie_a1b2c3d4e5f6", + "inbound_email_id", ) assert response.is_closed is True @@ -89,7 +89,7 @@ def test_raw_response_retrieve(self, client: CasParser) -> None: @parametrize def test_streaming_response_retrieve(self, client: CasParser) -> None: with client.inbound_email.with_streaming_response.retrieve( - "ie_a1b2c3d4e5f6", + "inbound_email_id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -240,7 +240,7 @@ async def test_streaming_response_create(self, async_client: AsyncCasParser) -> @parametrize async def test_method_retrieve(self, async_client: AsyncCasParser) -> None: inbound_email = await async_client.inbound_email.retrieve( - "ie_a1b2c3d4e5f6", + "inbound_email_id", ) assert_matches_type(InboundEmailRetrieveResponse, inbound_email, path=["response"]) @@ -248,7 +248,7 @@ async def test_method_retrieve(self, async_client: AsyncCasParser) -> None: @parametrize async def test_raw_response_retrieve(self, async_client: AsyncCasParser) -> None: response = await async_client.inbound_email.with_raw_response.retrieve( - "ie_a1b2c3d4e5f6", + "inbound_email_id", ) assert response.is_closed is True @@ -260,7 +260,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncCasParser) -> None @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncCasParser) -> None: async with async_client.inbound_email.with_streaming_response.retrieve( - "ie_a1b2c3d4e5f6", + "inbound_email_id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 6a7587856119da2a3118f985fb4cd8b13356b36e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:55:26 +0000 Subject: [PATCH 101/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cce9d1c..c523ce1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.7.0" + ".": "1.8.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 724085d..99d4afd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.7.0" +version = "1.8.0" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index e314464..e5ca681 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.7.0" # x-release-please-version +__version__ = "1.8.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index 3de2c14..82c5573 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.7.0" +version = "1.8.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From 702c840e66002294a7ed202520bcd79f94f67af1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 03:47:40 +0000 Subject: [PATCH 102/116] chore(internal): more robust bootstrap script --- scripts/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index 4638ec6..5a23841 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response From e85ea2c5e39f11740bf35ce5ec77041d2653c9e2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 04:36:49 +0000 Subject: [PATCH 103/116] fix: use correct field name format for multipart file arrays --- src/cas_parser/_qs.py | 8 ++----- src/cas_parser/_types.py | 3 +++ src/cas_parser/_utils/_utils.py | 42 ++++++++++++++++++++++++++------- tests/test_extract_files.py | 28 ++++++++++++++++++---- tests/test_files.py | 2 +- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/cas_parser/_qs.py b/src/cas_parser/_qs.py index de8c99b..4127c19 100644 --- a/src/cas_parser/_qs.py +++ b/src/cas_parser/_qs.py @@ -2,17 +2,13 @@ from typing import Any, List, Tuple, Union, Mapping, TypeVar from urllib.parse import parse_qs, urlencode -from typing_extensions import Literal, get_args +from typing_extensions import get_args -from ._types import NotGiven, not_given +from ._types import NotGiven, ArrayFormat, NestedFormat, not_given from ._utils import flatten _T = TypeVar("_T") - -ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] -NestedFormat = Literal["dots", "brackets"] - PrimitiveData = Union[str, int, float, bool, None] # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] # https://github.com/microsoft/pyright/issues/3555 diff --git a/src/cas_parser/_types.py b/src/cas_parser/_types.py index f3d52b8..29c3725 100644 --- a/src/cas_parser/_types.py +++ b/src/cas_parser/_types.py @@ -47,6 +47,9 @@ ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) _T = TypeVar("_T") +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + # Approximates httpx internal ProxiesTypes and RequestFiles types # while adding support for `PathLike` instances diff --git a/src/cas_parser/_utils/_utils.py b/src/cas_parser/_utils/_utils.py index 771859f..199cd23 100644 --- a/src/cas_parser/_utils/_utils.py +++ b/src/cas_parser/_utils/_utils.py @@ -17,11 +17,11 @@ ) from pathlib import Path from datetime import date, datetime -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, get_args import sniffio -from .._types import Omit, NotGiven, FileTypes, HeadersLike +from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -40,25 +40,45 @@ def extract_files( query: Mapping[str, object], *, paths: Sequence[Sequence[str]], + array_format: ArrayFormat = "brackets", ) -> list[tuple[str, FileTypes]]: """Recursively extract files from the given dictionary based on specified paths. A path may look like this ['foo', 'files', '', 'data']. + ``array_format`` controls how ```` segments contribute to the emitted + field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and + ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). + Note: this mutates the given dictionary. """ files: list[tuple[str, FileTypes]] = [] for path in paths: - files.extend(_extract_items(query, path, index=0, flattened_key=None)) + files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) return files +def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: + if array_format == "brackets": + return "[]" + if array_format == "indices": + return f"[{array_index}]" + if array_format == "repeat" or array_format == "comma": + # Both repeat the bare field name for each file part; there is no + # meaningful way to comma-join binary parts. + return "" + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + def _extract_items( obj: object, path: Sequence[str], *, index: int, flattened_key: str | None, + array_format: ArrayFormat, ) -> list[tuple[str, FileTypes]]: try: key = path[index] @@ -75,9 +95,11 @@ def _extract_items( if is_list(obj): files: list[tuple[str, FileTypes]] = [] - for entry in obj: - assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") - files.append((flattened_key + "[]", cast(FileTypes, entry))) + for array_index, entry in enumerate(obj): + suffix = _array_suffix(array_format, array_index) + emitted_key = (flattened_key + suffix) if flattened_key else suffix + assert_is_file_content(entry, key=emitted_key) + files.append((emitted_key, cast(FileTypes, entry))) return files assert_is_file_content(obj, key=flattened_key) @@ -106,6 +128,7 @@ def _extract_items( path, index=index, flattened_key=flattened_key, + array_format=array_format, ) elif is_list(obj): if key != "": @@ -117,9 +140,12 @@ def _extract_items( item, path, index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + flattened_key=( + (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) + ), + array_format=array_format, ) - for item in obj + for array_index, item in enumerate(obj) ] ) diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index bb4d8bb..6a123d0 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,7 +4,7 @@ import pytest -from cas_parser._types import FileTypes +from cas_parser._types import FileTypes, ArrayFormat from cas_parser._utils import extract_files @@ -37,10 +37,7 @@ def test_multiple_files() -> None: def test_top_level_file_array() -> None: query = {"files": [b"file one", b"file two"], "title": "hello"} - assert extract_files(query, paths=[["files", ""]]) == [ - ("files[]", b"file one"), - ("files[]", b"file two"), - ] + assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] assert query == {"title": "hello"} @@ -71,3 +68,24 @@ def test_ignores_incorrect_paths( expected: list[tuple[str, FileTypes]], ) -> None: assert extract_files(query, paths=paths) == expected + + +@pytest.mark.parametrize( + "array_format,expected_top_level,expected_nested", + [ + ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), + ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), + ], +) +def test_array_format_controls_file_field_names( + array_format: ArrayFormat, + expected_top_level: list[tuple[str, FileTypes]], + expected_nested: list[tuple[str, FileTypes]], +) -> None: + top_level = {"files": [b"a", b"b"]} + assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level + + nested = {"items": [{"file": b"a"}, {"file": b"b"}]} + assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py index 117534b..51cce7c 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -131,7 +131,7 @@ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: copied = deepcopy_with_paths(original, [["items", "", "file"]]) extracted = extract_files(copied, paths=[["items", "", "file"]]) - assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert [entry for _, entry in extracted] == [file1, file2] assert original == { "items": [ {"file": file1, "extra": 1}, From 6ea48adba15c0fd32845a651d350815954e55861 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 04:39:00 +0000 Subject: [PATCH 104/116] feat: support setting headers via env --- src/cas_parser/_client.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/cas_parser/_client.py b/src/cas_parser/_client.py index 1f11392..78096bd 100644 --- a/src/cas_parser/_client.py +++ b/src/cas_parser/_client.py @@ -19,7 +19,11 @@ RequestOptions, not_given, ) -from ._utils import is_given, get_async_library +from ._utils import ( + is_given, + is_mapping_t, + get_async_library, +) from ._compat import cached_property from ._models import SecurityOptions from ._version import __version__ @@ -115,6 +119,15 @@ def __init__( if base_url is None: base_url = f"https://api.casparser.in" + custom_headers_env = os.environ.get("CAS_PARSER_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, @@ -424,6 +437,15 @@ def __init__( if base_url is None: base_url = f"https://api.casparser.in" + custom_headers_env = os.environ.get("CAS_PARSER_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, From dec5da684bcf54e1378f0a5b0b2a67e98e883378 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 04:25:45 +0000 Subject: [PATCH 105/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index e26d438..4aee240 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-e5c0c65637cdf3a6c4360b8193973b73a3d35ad1056ef607c3319ef03e591a55.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-e5c0c65637cdf3a6c4360b8193973b73a3d35ad1056ef607c3319ef03e591a55.yml openapi_spec_hash: 7515d1e5fe3130b9f5411f7aacbc8a64 config_hash: 5509bb7a961ae2e79114b24c381606d4 From 5f9be89fc99c84504cc37eda0ea57ef858e39c5d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:25:19 +0000 Subject: [PATCH 106/116] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 4aee240..d5b8f44 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-e5c0c65637cdf3a6c4360b8193973b73a3d35ad1056ef607c3319ef03e591a55.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-2fd773786951b723a5d7d7342bf1c6ab46f08bd2851e916d188faae379d5aa4c.yml openapi_spec_hash: 7515d1e5fe3130b9f5411f7aacbc8a64 config_hash: 5509bb7a961ae2e79114b24c381606d4 From 95c989634e2482a15828d2e39ea9c57a607b7641 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:28:52 +0000 Subject: [PATCH 107/116] chore(internal): reformat pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 99d4afd..ebad659 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,7 +154,7 @@ show_error_codes = true # # We also exclude our `tests` as mypy doesn't always infer # types correctly and Pyright will still catch any type errors. -exclude = ['src/cas_parser/_files.py', '_dev/.*.py', 'tests/.*'] +exclude = ["src/cas_parser/_files.py", "_dev/.*.py", "tests/.*"] strict_equality = true implicit_reexport = true From 2ab97ef9b3cf6f4789f773cb49954987d4249ce3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 09:28:15 +0000 Subject: [PATCH 108/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c523ce1..c3c9552 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.8.0" + ".": "1.9.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ebad659..8d75d05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.8.0" +version = "1.9.0" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index e5ca681..97845a8 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.8.0" # x-release-please-version +__version__ = "1.9.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index 82c5573..62df07f 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.8.0" +version = "1.9.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From a87788ac50afe0f448b869e00f529d552fcf01c1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 10:18:10 +0000 Subject: [PATCH 109/116] feat(api): api update --- .stats.yml | 4 ++-- src/cas_parser/resources/inbound_email.py | 16 ++++++++-------- .../types/inbound_email_create_response.py | 10 +++++----- .../types/inbound_email_list_response.py | 10 +++++----- .../types/inbound_email_retrieve_response.py | 10 +++++----- .../types/inbox_list_cas_files_response.py | 7 ++++++- 6 files changed, 31 insertions(+), 26 deletions(-) diff --git a/.stats.yml b/.stats.yml index d5b8f44..cd58596 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-2fd773786951b723a5d7d7342bf1c6ab46f08bd2851e916d188faae379d5aa4c.yml -openapi_spec_hash: 7515d1e5fe3130b9f5411f7aacbc8a64 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-c7cca9a7a8e15f8a584c22eab142c4af72a329117f63cc3b3f7cabb25410f2ce.yml +openapi_spec_hash: f40d936e433bbf8c98179d0b36f304c8 config_hash: 5509bb7a961ae2e79114b24c381606d4 diff --git a/src/cas_parser/resources/inbound_email.py b/src/cas_parser/resources/inbound_email.py index 6028cba..75c1229 100644 --- a/src/cas_parser/resources/inbound_email.py +++ b/src/cas_parser/resources/inbound_email.py @@ -156,7 +156,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InboundEmailRetrieveResponse: """ - Retrieve details of a specific mailbox including statistics. + Retrieve details of a specific inbound email including statistics. Args: extra_headers: Send extra headers @@ -190,10 +190,10 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InboundEmailListResponse: - """List all mailboxes associated with your API key. + """List all inbound emails associated with your API key. - Returns active and inactive - mailboxes (deleted mailboxes are excluded). + Returns active and paused + inbound emails (deleted ones are excluded). Args: limit: Maximum number of inbound emails to return @@ -396,7 +396,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InboundEmailRetrieveResponse: """ - Retrieve details of a specific mailbox including statistics. + Retrieve details of a specific inbound email including statistics. Args: extra_headers: Send extra headers @@ -430,10 +430,10 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> InboundEmailListResponse: - """List all mailboxes associated with your API key. + """List all inbound emails associated with your API key. - Returns active and inactive - mailboxes (deleted mailboxes are excluded). + Returns active and paused + inbound emails (deleted ones are excluded). Args: limit: Maximum number of inbound emails to return diff --git a/src/cas_parser/types/inbound_email_create_response.py b/src/cas_parser/types/inbound_email_create_response.py index 77611a6..389d188 100644 --- a/src/cas_parser/types/inbound_email_create_response.py +++ b/src/cas_parser/types/inbound_email_create_response.py @@ -18,12 +18,12 @@ class InboundEmailCreateResponse(BaseModel): callback_url: Optional[str] = None """Webhook URL for email notifications. - `null` means files are only retrievable via `GET /v4/inbound-email/{id}/files` - (pull delivery). + Empty string (`""`) means files are only retrievable via + `GET /v4/inbound-email/{id}/files` (SDK / pull mode). """ created_at: Optional[datetime] = None - """When the mailbox was created""" + """When the inbound email was created""" email: Optional[str] = None """The inbound email address to forward CAS statements to""" @@ -38,7 +38,7 @@ class InboundEmailCreateResponse(BaseModel): """Your internal reference identifier""" status: Optional[Literal["active", "paused"]] = None - """Current mailbox status""" + """Current inbound email lifecycle status""" updated_at: Optional[datetime] = None - """When the mailbox was last updated""" + """When the inbound email was last updated""" diff --git a/src/cas_parser/types/inbound_email_list_response.py b/src/cas_parser/types/inbound_email_list_response.py index 2d700b6..896ebdb 100644 --- a/src/cas_parser/types/inbound_email_list_response.py +++ b/src/cas_parser/types/inbound_email_list_response.py @@ -18,12 +18,12 @@ class InboundEmail(BaseModel): callback_url: Optional[str] = None """Webhook URL for email notifications. - `null` means files are only retrievable via `GET /v4/inbound-email/{id}/files` - (pull delivery). + Empty string (`""`) means files are only retrievable via + `GET /v4/inbound-email/{id}/files` (SDK / pull mode). """ created_at: Optional[datetime] = None - """When the mailbox was created""" + """When the inbound email was created""" email: Optional[str] = None """The inbound email address to forward CAS statements to""" @@ -38,10 +38,10 @@ class InboundEmail(BaseModel): """Your internal reference identifier""" status: Optional[Literal["active", "paused"]] = None - """Current mailbox status""" + """Current inbound email lifecycle status""" updated_at: Optional[datetime] = None - """When the mailbox was last updated""" + """When the inbound email was last updated""" class InboundEmailListResponse(BaseModel): diff --git a/src/cas_parser/types/inbound_email_retrieve_response.py b/src/cas_parser/types/inbound_email_retrieve_response.py index cfcb7e5..7e397ff 100644 --- a/src/cas_parser/types/inbound_email_retrieve_response.py +++ b/src/cas_parser/types/inbound_email_retrieve_response.py @@ -18,12 +18,12 @@ class InboundEmailRetrieveResponse(BaseModel): callback_url: Optional[str] = None """Webhook URL for email notifications. - `null` means files are only retrievable via `GET /v4/inbound-email/{id}/files` - (pull delivery). + Empty string (`""`) means files are only retrievable via + `GET /v4/inbound-email/{id}/files` (SDK / pull mode). """ created_at: Optional[datetime] = None - """When the mailbox was created""" + """When the inbound email was created""" email: Optional[str] = None """The inbound email address to forward CAS statements to""" @@ -38,7 +38,7 @@ class InboundEmailRetrieveResponse(BaseModel): """Your internal reference identifier""" status: Optional[Literal["active", "paused"]] = None - """Current mailbox status""" + """Current inbound email lifecycle status""" updated_at: Optional[datetime] = None - """When the mailbox was last updated""" + """When the inbound email was last updated""" diff --git a/src/cas_parser/types/inbox_list_cas_files_response.py b/src/cas_parser/types/inbox_list_cas_files_response.py index 25b960f..aa8673d 100644 --- a/src/cas_parser/types/inbox_list_cas_files_response.py +++ b/src/cas_parser/types/inbox_list_cas_files_response.py @@ -16,7 +16,12 @@ class File(BaseModel): """Detected CAS provider based on sender email""" expires_in: Optional[int] = None - """URL expiration time in seconds (default 86400 = 24 hours)""" + """URL expiration time in seconds. Defaults vary by source: + + - Gmail Inbox Import: 86400 (24h) + - Inbound Email (webhook mode): 172800 (48h) + - Inbound Email (SDK mode): aligned with the session TTL (~30 min) + """ filename: Optional[str] = None """Standardized filename (provider_YYYYMMDD_uniqueid.pdf)""" From ee431c07c3d63657e2b9a1fbd3ee4bcab3cb2eed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 11:18:14 +0000 Subject: [PATCH 110/116] feat(api): api update --- .stats.yml | 4 ++-- src/cas_parser/types/inbound_email_create_response.py | 4 ++-- src/cas_parser/types/inbound_email_list_response.py | 4 ++-- src/cas_parser/types/inbound_email_retrieve_response.py | 4 ++-- src/cas_parser/types/inbox_list_cas_files_response.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index cd58596..d224bd8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-c7cca9a7a8e15f8a584c22eab142c4af72a329117f63cc3b3f7cabb25410f2ce.yml -openapi_spec_hash: f40d936e433bbf8c98179d0b36f304c8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-0dce3ce202e44ecf2270f6ca42942cc297bbeeafddb3128f8230ba3ae85c7551.yml +openapi_spec_hash: e9cef5743f686d9f12910c81832accca config_hash: 5509bb7a961ae2e79114b24c381606d4 diff --git a/src/cas_parser/types/inbound_email_create_response.py b/src/cas_parser/types/inbound_email_create_response.py index 389d188..b9de61d 100644 --- a/src/cas_parser/types/inbound_email_create_response.py +++ b/src/cas_parser/types/inbound_email_create_response.py @@ -18,8 +18,8 @@ class InboundEmailCreateResponse(BaseModel): callback_url: Optional[str] = None """Webhook URL for email notifications. - Empty string (`""`) means files are only retrievable via - `GET /v4/inbound-email/{id}/files` (SDK / pull mode). + If set, we POST each parsed email here. If omitted, files are only retrievable + via `GET /v4/inbound-email/{id}/files`. """ created_at: Optional[datetime] = None diff --git a/src/cas_parser/types/inbound_email_list_response.py b/src/cas_parser/types/inbound_email_list_response.py index 896ebdb..5f76158 100644 --- a/src/cas_parser/types/inbound_email_list_response.py +++ b/src/cas_parser/types/inbound_email_list_response.py @@ -18,8 +18,8 @@ class InboundEmail(BaseModel): callback_url: Optional[str] = None """Webhook URL for email notifications. - Empty string (`""`) means files are only retrievable via - `GET /v4/inbound-email/{id}/files` (SDK / pull mode). + If set, we POST each parsed email here. If omitted, files are only retrievable + via `GET /v4/inbound-email/{id}/files`. """ created_at: Optional[datetime] = None diff --git a/src/cas_parser/types/inbound_email_retrieve_response.py b/src/cas_parser/types/inbound_email_retrieve_response.py index 7e397ff..4657e4d 100644 --- a/src/cas_parser/types/inbound_email_retrieve_response.py +++ b/src/cas_parser/types/inbound_email_retrieve_response.py @@ -18,8 +18,8 @@ class InboundEmailRetrieveResponse(BaseModel): callback_url: Optional[str] = None """Webhook URL for email notifications. - Empty string (`""`) means files are only retrievable via - `GET /v4/inbound-email/{id}/files` (SDK / pull mode). + If set, we POST each parsed email here. If omitted, files are only retrievable + via `GET /v4/inbound-email/{id}/files`. """ created_at: Optional[datetime] = None diff --git a/src/cas_parser/types/inbox_list_cas_files_response.py b/src/cas_parser/types/inbox_list_cas_files_response.py index aa8673d..c4d1c62 100644 --- a/src/cas_parser/types/inbox_list_cas_files_response.py +++ b/src/cas_parser/types/inbox_list_cas_files_response.py @@ -19,8 +19,8 @@ class File(BaseModel): """URL expiration time in seconds. Defaults vary by source: - Gmail Inbox Import: 86400 (24h) - - Inbound Email (webhook mode): 172800 (48h) - - Inbound Email (SDK mode): aligned with the session TTL (~30 min) + - Inbound Email with `callback_url` set: 172800 (48h) + - Inbound Email without `callback_url`: aligned with the session TTL (~30 min) """ filename: Optional[str] = None From c141e99c804d096b3d481281140f53db2c77082c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 23:18:15 +0000 Subject: [PATCH 111/116] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d224bd8..582a92c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-0dce3ce202e44ecf2270f6ca42942cc297bbeeafddb3128f8230ba3ae85c7551.yml -openapi_spec_hash: e9cef5743f686d9f12910c81832accca +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-e572d88c2af6e4d7bc4f7e119357fd3f68b1e67d612fd1d3a657d916cde0087c.yml +openapi_spec_hash: a9fc7d947111bffa9184f8ca8be4a579 config_hash: 5509bb7a961ae2e79114b24c381606d4 From b0d5ce60227a46fb21e2a6fa15a753590c1302dc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 03:11:40 +0000 Subject: [PATCH 112/116] fix(client): add missing f-string prefix in file type error message --- src/cas_parser/_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cas_parser/_files.py b/src/cas_parser/_files.py index 0fdce17..76da9e0 100644 --- a/src/cas_parser/_files.py +++ b/src/cas_parser/_files.py @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles elif is_sequence_t(files): files = [(key, await _async_transform_file(file)) for key, file in files] else: - raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") return files From 719184d8a5ee57fa945962c9d80861a5e16c1307 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 13:21:09 +0000 Subject: [PATCH 113/116] chore(internal): version bump --- .release-please-manifest.json | 2 +- pyproject.toml | 2 +- src/cas_parser/_version.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c3c9552..eb4e0db 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.9.0" + ".": "1.10.0" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8d75d05..7916b18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cas-parser-python" -version = "1.9.0" +version = "1.10.0" description = "The official Python library for the cas-parser API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/cas_parser/_version.py b/src/cas_parser/_version.py index 97845a8..43c27c5 100644 --- a/src/cas_parser/_version.py +++ b/src/cas_parser/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "cas_parser" -__version__ = "1.9.0" # x-release-please-version +__version__ = "1.10.0" # x-release-please-version diff --git a/uv.lock b/uv.lock index 62df07f..03b79de 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "cas-parser-python" -version = "1.9.0" +version = "1.10.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From 417283f830800e532aa7d6c64de8116ce30fe229 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 03:03:31 +0000 Subject: [PATCH 114/116] feat(internal/types): support eagerly validating pydantic iterators --- src/cas_parser/_models.py | 80 +++++++++++++++++++++++++++++++++++++++ tests/test_models.py | 60 +++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/src/cas_parser/_models.py b/src/cas_parser/_models.py index 6df43eb..ca8639a 100644 --- a/src/cas_parser/_models.py +++ b/src/cas_parser/_models.py @@ -25,7 +25,9 @@ ClassVar, Protocol, Required, + Annotated, ParamSpec, + TypeAlias, TypedDict, TypeGuard, final, @@ -79,7 +81,15 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: + from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler + from pydantic_core import CoreSchema, core_schema from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema +else: + try: + from pydantic_core import CoreSchema, core_schema + except ImportError: + CoreSchema = None + core_schema = None __all__ = ["BaseModel", "GenericModel"] @@ -396,6 +406,76 @@ def model_dump_json( ) +class _EagerIterable(list[_T], Generic[_T]): + """ + Accepts any Iterable[T] input (including generators), consumes it + eagerly, and validates all items upfront. + + Validation preserves the original container type where possible + (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON) + always emits a list — round-tripping through model_dump() will not + restore the original container type. + """ + + @classmethod + def __get_pydantic_core_schema__( + cls, + source_type: Any, + handler: GetCoreSchemaHandler, + ) -> CoreSchema: + (item_type,) = get_args(source_type) or (Any,) + item_schema: CoreSchema = handler.generate_schema(item_type) + list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema) + + return core_schema.no_info_wrap_validator_function( + cls._validate, + list_of_items_schema, + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, + info_arg=False, + ), + ) + + @staticmethod + def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any: + original_type: type[Any] = type(v) + + # Normalize to list so list_schema can validate each item + if isinstance(v, list): + items: list[_T] = v + else: + try: + items = list(v) + except TypeError as e: + raise TypeError("Value is not iterable") from e + + # Validate items against the inner schema + validated: list[_T] = handler(items) + + # Reconstruct original container type + if original_type is list: + return validated + # str(list) produces the list's repr, not a string built from items, + # so skip reconstruction for str and its subclasses. + if issubclass(original_type, str): + return validated + try: + return original_type(validated) + except (TypeError, ValueError): + # If the type cannot be reconstructed, just return the validated list + return validated + + @staticmethod + def _serialize(v: Iterable[_T]) -> list[_T]: + """Always serialize as a list so Pydantic's JSON encoder is happy.""" + if isinstance(v, list): + return v + return list(v) + + +EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable] + + def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) diff --git a/tests/test_models.py b/tests/test_models.py index 82ce6d4..92e7ace 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,8 @@ import json -from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated, TypeAliasType +from collections import deque +from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType import pytest import pydantic @@ -9,7 +10,7 @@ from cas_parser._utils import PropertyInfo from cas_parser._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from cas_parser._models import DISCRIMINATOR_CACHE, BaseModel, construct_type +from cas_parser._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type class BasicModel(BaseModel): @@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ... assert model.a.prop == 1 assert isinstance(model.a, Item) assert model.other == "foo" + + +# NOTE: Workaround for Pydantic Iterable behavior. +# Iterable fields are replaced with a ValidatorIterator and may be consumed +# during serialization, which can cause subsequent dumps to return empty data. +# See: https://github.com/pydantic/pydantic/issues/9541 +@pytest.mark.parametrize( + "data, expected_validated", + [ + ([1, 2, 3], [1, 2, 3]), + ((1, 2, 3), (1, 2, 3)), + (set([1, 2, 3]), set([1, 2, 3])), + (iter([1, 2, 3]), [1, 2, 3]), + ([], []), + ((x for x in [1, 2, 3]), [1, 2, 3]), + (map(lambda x: x, [1, 2, 3]), [1, 2, 3]), + (frozenset([1, 2, 3]), frozenset([1, 2, 3])), + (deque([1, 2, 3]), deque([1, 2, 3])), + ], + ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"], +) +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None: + class TypeWithIterable(TypedDict): + items: EagerIterable[int] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": data}}) + assert m.data["items"] == expected_validated + + # Verify repeated dumps don't lose data (the original bug) + assert m.model_dump()["data"]["items"] == list(expected_validated) + assert m.model_dump()["data"]["items"] == list(expected_validated) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction_str_falls_back_to_list() -> None: + # str is iterable (over chars), but str(list_of_chars) produces the list's repr + # rather than reconstructing a string from items. We special-case str to fall + # back to list instead of attempting reconstruction. + class TypeWithIterable(TypedDict): + items: EagerIterable[str] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": "hello"}}) + + # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"]) + assert m.data["items"] == ["h", "e", "l", "l", "o"] + assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"] From f5d37dd9aa4c9ed2b86b8379a1eb124a41c297ed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 02:36:51 +0000 Subject: [PATCH 115/116] ci: pin GitHub Actions to commit SHAs Pin all GitHub Actions referenced in generated workflows (both first-party `actions/*` and third-party) to immutable commit SHAs. Updating pinned actions is now a deliberate codegen-side bump rather than implicit on every workflow run. --- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/publish-pypi.yml | 4 ++-- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7923085..4d9b084 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,10 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: '0.10.2' @@ -43,10 +43,10 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: '0.10.2' @@ -61,7 +61,7 @@ jobs: github.repository == 'stainless-sdks/cas-parser-python' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -81,10 +81,10 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/cas-parser-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: '0.10.2' diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 92d0c62..c0b2eba 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -17,10 +17,10 @@ jobs: id-token: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: '0.9.13' diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index c918745..7a6c03c 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'CASParser/cas-parser-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check release environment run: | From 15764a2b9b5cd153c05daa8d92b74addbadbf3ca Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 10:17:51 +0000 Subject: [PATCH 116/116] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 582a92c..fb087e4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 21 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-e572d88c2af6e4d7bc4f7e119357fd3f68b1e67d612fd1d3a657d916cde0087c.yml -openapi_spec_hash: a9fc7d947111bffa9184f8ca8be4a579 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser/cas-parser-904e3aa8081755d046016db9d84d13d140a4235c724e18e1cd7f8ebb7712883e.yml +openapi_spec_hash: 453b8e667c364b064e04352ad4deccfa config_hash: 5509bb7a961ae2e79114b24c381606d4