From 55ac0abebdce76e0e0563ee586ec40e238e599cf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:45:55 +0000 Subject: [PATCH 01/11] build: require Python 3.11 and server models Co-Authored-By: AJ Steers --- .github/workflows/autofix.yml | 2 +- .github/workflows/fix-pr-command.yml | 2 +- .github/workflows/pydoc_preview.yml | 2 +- .github/workflows/pydoc_publish.yml | 2 +- .github/workflows/python_lint.yml | 8 +- .github/workflows/python_pytest.yml | 11 +- .github/workflows/release_drafter.yml | 2 +- .github/workflows/test-pr-command.yml | 1 - .github/workflows/uv-lock-command.yml | 2 +- .ruff.toml | 2 +- README.md | 4 +- pyproject.toml | 11 +- pyrefly.toml | 2 +- uv.lock | 526 +------------------------- 14 files changed, 42 insertions(+), 535 deletions(-) diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 03ee649a1..2159659ee 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -21,7 +21,7 @@ jobs: uses: astral-sh/setup-uv@f06b870e0a91d23284a3013acc55e6f88ab4b904 # v7 with: version: "latest" - python-version: '3.10' + python-version: '3.11' - name: Install dev dependencies # We only need helper libraries diff --git a/.github/workflows/fix-pr-command.yml b/.github/workflows/fix-pr-command.yml index 171564cea..ff91cd6f7 100644 --- a/.github/workflows/fix-pr-command.yml +++ b/.github/workflows/fix-pr-command.yml @@ -29,7 +29,7 @@ jobs: strategy: matrix: python-version: [ - '3.10', + '3.11', ] os: [ Ubuntu, diff --git a/.github/workflows/pydoc_preview.yml b/.github/workflows/pydoc_preview.yml index cc92dd32d..cba9061c8 100644 --- a/.github/workflows/pydoc_preview.yml +++ b/.github/workflows/pydoc_preview.yml @@ -23,7 +23,7 @@ jobs: uses: astral-sh/setup-uv@f06b870e0a91d23284a3013acc55e6f88ab4b904 # v7 with: version: "latest" - python-version: '3.10' + python-version: '3.11' - name: Install dev dependencies # We only need helper libraries diff --git a/.github/workflows/pydoc_publish.yml b/.github/workflows/pydoc_publish.yml index 20a526f08..db8198f4c 100644 --- a/.github/workflows/pydoc_publish.yml +++ b/.github/workflows/pydoc_publish.yml @@ -37,7 +37,7 @@ jobs: uses: astral-sh/setup-uv@f06b870e0a91d23284a3013acc55e6f88ab4b904 # v7 with: version: "latest" - python-version: '3.10' + python-version: '3.11' - name: Setup Pages uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 diff --git a/.github/workflows/python_lint.yml b/.github/workflows/python_lint.yml index 3401d8fef..8cddeeebe 100644 --- a/.github/workflows/python_lint.yml +++ b/.github/workflows/python_lint.yml @@ -23,7 +23,7 @@ jobs: uses: astral-sh/setup-uv@f06b870e0a91d23284a3013acc55e6f88ab4b904 # v7 with: version: "latest" - python-version: '3.10' + python-version: '3.11' - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -43,7 +43,7 @@ jobs: uses: astral-sh/setup-uv@f06b870e0a91d23284a3013acc55e6f88ab4b904 # v7 with: version: "latest" - python-version: '3.10' + python-version: '3.11' - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -63,7 +63,7 @@ jobs: uses: astral-sh/setup-uv@f06b870e0a91d23284a3013acc55e6f88ab4b904 # v7 with: version: "latest" - python-version: '3.10' + python-version: '3.11' - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -85,7 +85,7 @@ jobs: uses: astral-sh/setup-uv@f06b870e0a91d23284a3013acc55e6f88ab4b904 # v7 with: version: "latest" - python-version: '3.10' + python-version: '3.11' - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev diff --git a/.github/workflows/python_pytest.yml b/.github/workflows/python_pytest.yml index 18288d616..bf44d0201 100644 --- a/.github/workflows/python_pytest.yml +++ b/.github/workflows/python_pytest.yml @@ -43,7 +43,7 @@ jobs: uses: astral-sh/setup-uv@f06b870e0a91d23284a3013acc55e6f88ab4b904 # v7 with: version: "latest" - python-version: '3.10' + python-version: '3.11' - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -117,7 +117,7 @@ jobs: uses: astral-sh/setup-uv@f06b870e0a91d23284a3013acc55e6f88ab4b904 # v7 with: version: "latest" - python-version: '3.10' + python-version: '3.11' - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -176,7 +176,6 @@ jobs: strategy: matrix: python-version: [ - '3.10', '3.11', # '3.12', # Commented out: Certain tests don't work in Python 3.12, although PyAirbyte itself does work on 3.12 ] @@ -251,7 +250,7 @@ jobs: path: htmlcov/ - name: Upload coverage to GitHub Code Quality - if: always() && matrix.python-version == '3.10' && matrix.os == 'Ubuntu' + if: always() && matrix.python-version == '3.11' && matrix.os == 'Ubuntu' uses: actions/upload-code-coverage@b51da2c3c1b23e04d2d6477cfc34350b1f5cd3e9 # v1 with: file: htmlcov/coverage.xml @@ -268,7 +267,7 @@ jobs: - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() && !cancelled() && matrix.python-version == '3.10' && matrix.os == 'Ubuntu' + if: always() && !cancelled() && matrix.python-version == '3.11' && matrix.os == 'Ubuntu' continue-on-error: true with: check_name: "PyTest Results (Full)" @@ -286,7 +285,7 @@ jobs: uses: astral-sh/setup-uv@f06b870e0a91d23284a3013acc55e6f88ab4b904 # v7 with: version: "latest" - python-version: '3.10' + python-version: '3.11' - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev diff --git a/.github/workflows/release_drafter.yml b/.github/workflows/release_drafter.yml index 902bf63df..051dda078 100644 --- a/.github/workflows/release_drafter.yml +++ b/.github/workflows/release_drafter.yml @@ -22,7 +22,7 @@ jobs: uses: astral-sh/setup-uv@v7 with: version: "latest" - python-version: "3.10" + python-version: "3.11" - name: Checkout Repo uses: actions/checkout@v6 diff --git a/.github/workflows/test-pr-command.yml b/.github/workflows/test-pr-command.yml index ceaf29eb6..338b7ef2e 100644 --- a/.github/workflows/test-pr-command.yml +++ b/.github/workflows/test-pr-command.yml @@ -61,7 +61,6 @@ jobs: strategy: matrix: python-version: [ - '3.10', '3.11', ] os: [ diff --git a/.github/workflows/uv-lock-command.yml b/.github/workflows/uv-lock-command.yml index 8c8423181..0f09317d9 100644 --- a/.github/workflows/uv-lock-command.yml +++ b/.github/workflows/uv-lock-command.yml @@ -26,7 +26,7 @@ jobs: strategy: matrix: python-version: [ - '3.10', + '3.11', ] os: [ Ubuntu, diff --git a/.ruff.toml b/.ruff.toml index f0b61926a..114eaffb4 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,6 +1,6 @@ # Ruff configuration file # For rules reference, see https://docs.astral.sh/ruff/rules/ -target-version = "py310" +target-version = "py311" preview = true line-length = 100 diff --git a/README.md b/README.md index 1ea8d3f48..08ce821bb 100644 --- a/README.md +++ b/README.md @@ -44,14 +44,14 @@ If you prefer to fall back to the prior `pip`-based installation methods, set th In both `get_source()` and `get_destination()`, you can provide a `use_python` input arg that is equal to the desired version of Python that you with to use for the given connector. This can be helpful if an older connector doesn't support the version of Python that you are using for PyAirbyte itself. -For example, assuming PyAirbyte is running on Python 3.12, you can install a connector using Python 3.10.13 with the following code snippet: +For example, assuming PyAirbyte is running on Python 3.12, you can install a connector using Python 3.11.13 with the following code snippet: ```py import airbyte as ab source = ab.get_source( "source-faker", - use_python="3.10.17", + use_python="3.11.17", ) ``` diff --git a/pyproject.toml b/pyproject.toml index 3adc0cfec..5c36fd399 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,12 +5,13 @@ readme = "README.md" authors = [ { name = "Airbyte", email = "contact@airbyte.io" } ] -requires-python = ">=3.10,<3.13" +requires-python = ">=3.11,<3.13" dynamic = ["version"] dependencies = [ "airbyte-api>=0.53.0,<1.0", "airbyte-cdk>=7.28.3,<8.0.0", "airbyte-protocol-models-pdv2>=0.13.0,<1.0", + "airbyte-server-models", "cryptography>=44.0.0,<47.0.0,!=45.0.0,!=45.0.1", # 45.0.0/45.0.1 broke load_pem_private_key() "cyclopts>=4.0,<5.0", # TODO: Restore broader version constraints after verifying compatibility @@ -104,6 +105,12 @@ fallback-version = "0.0.0" [tool.hatch.metadata] allow-direct-references = true +[tool.uv.sources] +# TK-TODO: `airbyte-server-models` is consumed from a git branch while the package is +# unreleased. Pin this to a published PyPI version (and remove this source entry) +# before the next PyAirbyte release. +airbyte-server-models = { git = "https://github.com/airbytehq/airbyte-ops-mcp.git", branch = "devin/1788977312-server-models", subdirectory = "airbyte-server-models" } + [tool.hatch.build.targets.wheel] packages = ["airbyte"] @@ -148,7 +155,7 @@ filterwarnings = [ # syntax: "action:message_regex:category:module:line" ignore_errors = true [tool.pyright] -pythonVersion = "3.10" +pythonVersion = "3.11" venvPath = "./" # Assuming .venv is at the root of your project venv = ".venv" diff --git a/pyrefly.toml b/pyrefly.toml index 8c1cf32e1..a43356d63 100644 --- a/pyrefly.toml +++ b/pyrefly.toml @@ -1,7 +1,7 @@ # Pyrefly Type Checker Configuration # https://pyrefly.org/en/docs/configuration/ -python-version = "3.10" +python-version = "3.11" project-includes = ["airbyte"] project-excludes = [ "tests/integration_tests/fixtures/source-broken/**", diff --git a/uv.lock b/uv.lock index 5215260c5..04f75bf1e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.10, <3.13" +requires-python = ">=3.11, <3.13" resolution-markers = [ "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", + "python_full_version < '3.12'", ] [[package]] @@ -47,7 +46,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, - { name = "async-timeout", marker = "python_full_version < '3.11'" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, @@ -56,23 +54,6 @@ dependencies = [ ] 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" }, @@ -129,6 +110,7 @@ dependencies = [ { name = "airbyte-api" }, { name = "airbyte-cdk" }, { name = "airbyte-protocol-models-pdv2" }, + { name = "airbyte-server-models" }, { name = "cryptography" }, { name = "cyclopts" }, { name = "duckdb" }, @@ -196,6 +178,7 @@ requires-dist = [ { name = "airbyte-api", specifier = ">=0.53.0,<1.0" }, { name = "airbyte-cdk", specifier = ">=7.28.3,<8.0.0" }, { name = "airbyte-protocol-models-pdv2", specifier = ">=0.13.0,<1.0" }, + { name = "airbyte-server-models", git = "https://github.com/airbytehq/airbyte-ops-mcp.git?subdirectory=airbyte-server-models&branch=devin%2F1788977312-server-models" }, { name = "cryptography", specifier = ">=44.0.0,!=45.0.0,!=45.0.1,<47.0.0" }, { name = "cyclopts", specifier = ">=4.0,<5.0" }, { name = "duckdb", specifier = "==1.4.3" }, @@ -356,6 +339,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/24/414fe44348ca7c7ae71030012e77302b3e41d4ee217cc5931dabfbf90496/airbyte_protocol_models_pdv2-0.18.0-py3-none-any.whl", hash = "sha256:62b08659b3da0f03316c60d84736f12dbfd79340a49947155a1687c13314db8b", size = 12805, upload-time = "2025-06-27T21:01:35.637Z" }, ] +[[package]] +name = "airbyte-server-models" +version = "0.0.0.post792.dev0+85882b5" +source = { git = "https://github.com/airbytehq/airbyte-ops-mcp.git?subdirectory=airbyte-server-models&branch=devin%2F1788977312-server-models#85882b5e45fdb2b6b0ca5c5844f4f5e1ab8c9a44" } +dependencies = [ + { name = "pydantic", extra = ["email"] }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -398,7 +389,6 @@ name = "anyio" version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions" }, ] @@ -425,15 +415,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" }, ] -[[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 = "attributes-doc" version = "0.4.0" @@ -552,10 +533,6 @@ version = "0.9.25" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" }, - { url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ab/07080ecb1adb55a02cbd8ec0126aa8e43af343ffabb6a71125b42670e9a1/caio-0.9.25-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:bf61d7d0c4fd10ffdd98ca47f7e8db4d7408e74649ffaf4bef40b029ada3c21b", size = 79457, upload-time = "2026-03-04T22:08:16.024Z" }, - { url = "https://files.pythonhosted.org/packages/88/95/dd55757bb671eb4c376e006c04e83beb413486821f517792ea603ef216e9/caio-0.9.25-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:ab52e5b643f8bbd64a0605d9412796cd3464cb8ca88593b13e95a0f0b10508ae", size = 77705, upload-time = "2026-03-04T22:08:17.202Z" }, { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" }, { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" }, { url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" }, @@ -573,7 +550,6 @@ version = "25.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" } @@ -599,18 +575,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, @@ -642,22 +606,6 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, @@ -739,18 +687,6 @@ version = "7.13.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, - { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, - { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, - { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, - { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, - { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, - { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, - { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, - { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, - { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, @@ -813,12 +749,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067, upload-time = "2025-05-02T19:35:31.547Z" }, { url = "https://files.pythonhosted.org/packages/a7/6c/d2c48c8137eb39d0c193274db5c04a75dab20d2f7c3f81a7dcc3a8897701/cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028", size = 2775467, upload-time = "2025-05-02T19:35:33.805Z" }, { url = "https://files.pythonhosted.org/packages/c9/ad/51f212198681ea7b0deaaf8846ee10af99fba4e894f67b353524eab2bbe5/cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334", size = 3210375, upload-time = "2025-05-02T19:35:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/7f/10/abcf7418536df1eaba70e2cfc5c8a0ab07aa7aa02a5cbc6a78b9d8b4f121/cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d", size = 3393192, upload-time = "2025-05-02T19:35:37.468Z" }, - { url = "https://files.pythonhosted.org/packages/06/59/ecb3ef380f5891978f92a7f9120e2852b1df6f0a849c277b8ea45b865db2/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8", size = 3898419, upload-time = "2025-05-02T19:35:39.065Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d0/35e2313dbb38cf793aa242182ad5bc5ef5c8fd4e5dbdc380b936c7d51169/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4", size = 4117892, upload-time = "2025-05-02T19:35:40.839Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c8/31fb6e33b56c2c2100d76de3fd820afaa9d4d0b6aea1ccaf9aaf35dc7ce3/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff", size = 3900855, upload-time = "2025-05-02T19:35:42.599Z" }, - { url = "https://files.pythonhosted.org/packages/43/2a/08cc2ec19e77f2a3cfa2337b429676406d4bb78ddd130a05c458e7b91d73/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06", size = 4117619, upload-time = "2025-05-02T19:35:44.774Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/fc3d3f84022a75f2ac4b1a1c0e5d6a0c2ea259e14cd4aae3e0e68e56483c/cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9", size = 3136570, upload-time = "2025-05-02T19:35:46.94Z" }, { url = "https://files.pythonhosted.org/packages/8d/4b/c11ad0b6c061902de5223892d680e89c06c7c4d606305eb8de56c5427ae6/cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375", size = 3390230, upload-time = "2025-05-02T19:35:49.062Z" }, { url = "https://files.pythonhosted.org/packages/58/11/0a6bf45d53b9b2290ea3cec30e78b78e6ca29dc101e2e296872a0ffe1335/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647", size = 3895216, upload-time = "2025-05-02T19:35:51.351Z" }, { url = "https://files.pythonhosted.org/packages/0a/27/b28cdeb7270e957f0077a2c2bfad1b38f72f1f6d699679f97b816ca33642/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259", size = 4115044, upload-time = "2025-05-02T19:35:53.044Z" }, @@ -836,8 +766,6 @@ dependencies = [ { name = "docstring-parser" }, { name = "rich" }, { name = "rich-rst" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/7b/663f3285c1ac0e5d0854bd9db2c87caa6fa3d1a063185e3394a6cdca9151/cyclopts-4.5.0.tar.gz", hash = "sha256:717ac4235548b58d500baf7e688aa4d024caf0ee68f61a012ffd5e29db3099f9", size = 161980, upload-time = "2026-01-16T02:07:16.171Z" } wheels = [ @@ -881,7 +809,6 @@ dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "packaging" }, { name = "requirements-parser" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/aa/5cae0f25a2ac5334d5bd2782a6bcd80eecf184f433ff74b2fb0387cfbbb6/deptry-0.24.0.tar.gz", hash = "sha256:852e88af2087e03cdf9ece6916f3f58b74191ab51cc8074897951bd496ee7dbb", size = 440158, upload-time = "2025-11-09T00:31:44.637Z" } wheels = [ @@ -967,12 +894,6 @@ version = "1.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7f/da/17c3eb5458af69d54dedc8d18e4a32ceaa8ce4d4c699d45d6d8287e790c3/duckdb-1.4.3.tar.gz", hash = "sha256:fea43e03604c713e25a25211ada87d30cd2a044d8f27afab5deba26ac49e5268", size = 18478418, upload-time = "2025-12-09T10:59:22.945Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/3a/ea8e237e1ba40203dea4ed6a8798ea51e66a4c4f34605697025e5fa06fdd/duckdb-1.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:efa7f1191c59e34b688fcd4e588c1b903a4e4e1f4804945902cf0b20e08a9001", size = 29016021, upload-time = "2025-12-09T10:57:46.847Z" }, - { url = "https://files.pythonhosted.org/packages/48/88/07615298a2871362b454237b6a2d7724e6ba0afba2bddedddde5bbf129d5/duckdb-1.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fef6a053a1c485292000bf0c338bba60f89d334f6a06fc76ba4085a5a322b76", size = 15405906, upload-time = "2025-12-09T10:57:49.213Z" }, - { url = "https://files.pythonhosted.org/packages/fa/66/b407ab3cd4822191aa5defb27522213b6ba670437c7da09a062d8b75b0a4/duckdb-1.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:702dabbc22b27dc5b73e7599c60deef3d8c59968527c36b391773efddd8f4cf1", size = 13732991, upload-time = "2025-12-09T10:57:51.189Z" }, - { url = "https://files.pythonhosted.org/packages/33/f0/e8edab80446d87b4e0faf3aaa440f9cfd9d0609c21a4be56174c8ba7d23c/duckdb-1.4.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854b79375fa618f6ffa8d84fb45cbc9db887f6c4834076ea10d20bc106f1fd90", size = 18471503, upload-time = "2025-12-09T10:57:53.186Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7a/8d257bc847f0ac6a6639ae0a6e7f35f0b5bfbae472ee4846ee32404670a6/duckdb-1.4.3-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bb8bd5a3dd205983726185b280a211eacc9f5bc0c4d4505bec8c87ac33a8ccb", size = 20466012, upload-time = "2025-12-09T10:57:55.702Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d1/8f6bdaf2da6a076dd63c84ed87fb82d0741c9f4acb3dd476d73ca0a08ffe/duckdb-1.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:d0ff08388ef8b1d1a4c95c321d6c5fa11201b241036b1ee740f9d841df3d6ba2", size = 12328392, upload-time = "2025-12-09T10:57:57.718Z" }, { url = "https://files.pythonhosted.org/packages/ec/bc/7c5e50e440c8629495678bc57bdfc1bb8e62f61090f2d5441e2bd0a0ed96/duckdb-1.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:366bf607088053dce845c9d24c202c04d78022436cc5d8e4c9f0492de04afbe7", size = 29019361, upload-time = "2025-12-09T10:57:59.845Z" }, { url = "https://files.pythonhosted.org/packages/26/15/c04a4faf0dfddad2259cab72bf0bd4b3d010f2347642541bd254d516bf93/duckdb-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d080e8d1bf2d226423ec781f539c8f6b6ef3fd42a9a58a7160de0a00877a21f", size = 15407465, upload-time = "2025-12-09T10:58:02.465Z" }, { url = "https://files.pythonhosted.org/packages/cb/54/a049490187c9529932fc153f7e1b92a9e145586281fe4e03ce0535a0497c/duckdb-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9dc049ba7e906cb49ca2b6d4fbf7b6615ec3883193e8abb93f0bef2652e42dda", size = 13735781, upload-time = "2025-12-09T10:58:04.847Z" }, @@ -1076,12 +997,6 @@ version = "1.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/8b/fa2d3287fd2267be6261d0177c6809a7fa12c5600ddb33490c8dc29e77b2/fastavro-1.12.1.tar.gz", hash = "sha256:2f285be49e45bc047ab2f6bed040bb349da85db3f3c87880e4b92595ea093b2b", size = 1025661, upload-time = "2025-10-10T15:40:55.41Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/a0/077fd7cbfc143152cb96780cb592ed6cb6696667d8bc1b977745eb2255a8/fastavro-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:00650ca533907361edda22e6ffe8cf87ab2091c5d8aee5c8000b0f2dcdda7ed3", size = 1000335, upload-time = "2025-10-10T15:40:59.834Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ae/a115e027f3a75df237609701b03ecba0b7f0aa3d77fe0161df533fde1eb7/fastavro-1.12.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac76d6d95f909c72ee70d314b460b7e711d928845771531d823eb96a10952d26", size = 3221067, upload-time = "2025-10-10T15:41:04.399Z" }, - { url = "https://files.pythonhosted.org/packages/94/4e/c4991c3eec0175af9a8a0c161b88089cb7bf7fe353b3e3be1bc4cf9036b2/fastavro-1.12.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55eef18c41d4476bd32a82ed5dd86aabc3f614e1b66bdb09ffa291612e1670", size = 3228979, upload-time = "2025-10-10T15:41:06.738Z" }, - { url = "https://files.pythonhosted.org/packages/21/0c/f2afb8eaea38799ccb1ed07d68bf2659f2e313f1902bbd36774cf6a1bef9/fastavro-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81563e1f93570e6565487cdb01ba241a36a00e58cff9c5a0614af819d1155d8f", size = 3160740, upload-time = "2025-10-10T15:41:08.731Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1a/f4d367924b40b86857862c1fa65f2afba94ddadf298b611e610a676a29e5/fastavro-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec207360f76f0b3de540758a297193c5390e8e081c43c3317f610b1414d8c8f", size = 3235787, upload-time = "2025-10-10T15:41:10.869Z" }, - { url = "https://files.pythonhosted.org/packages/90/ec/8db9331896e3dfe4f71b2b3c23f2e97fbbfd90129777467ca9f8bafccb74/fastavro-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:c0390bfe4a9f8056a75ac6785fbbff8f5e317f5356481d2e29ec980877d2314b", size = 449350, upload-time = "2025-10-10T15:41:12.104Z" }, { url = "https://files.pythonhosted.org/packages/a0/e9/31c64b47cefc0951099e7c0c8c8ea1c931edd1350f34d55c27cbfbb08df1/fastavro-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b632b713bc5d03928a87d811fa4a11d5f25cd43e79c161e291c7d3f7aa740fd", size = 1016585, upload-time = "2025-10-10T15:41:13.717Z" }, { url = "https://files.pythonhosted.org/packages/10/76/111560775b548f5d8d828c1b5285ff90e2d2745643fb80ecbf115344eea4/fastavro-1.12.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa7ab3769beadcebb60f0539054c7755f63bd9cf7666e2c15e615ab605f89a8", size = 3404629, upload-time = "2025-10-10T15:41:15.642Z" }, { url = "https://files.pythonhosted.org/packages/b0/07/6bb93cb963932146c2b6c5c765903a0a547ad9f0f8b769a4a9aad8c06369/fastavro-1.12.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123fb221df3164abd93f2d042c82f538a1d5a43ce41375f12c91ce1355a9141e", size = 3428594, upload-time = "2025-10-10T15:41:17.779Z" }, @@ -1176,22 +1091,6 @@ 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" }, @@ -1368,11 +1267,6 @@ version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff", size = 31296, upload-time = "2025-12-16T00:19:07.261Z" }, - { url = "https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288", size = 30870, upload-time = "2025-12-16T00:29:17.669Z" }, - { url = "https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d", size = 33214, upload-time = "2025-12-16T00:40:19.618Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/11b70614df04c289128d782efc084b9035ef8466b3d0a8757c1b6f5cf7ac/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092", size = 33589, upload-time = "2025-12-16T00:40:20.7Z" }, - { url = "https://files.pythonhosted.org/packages/3e/00/a08a4bc24f1261cc5b0f47312d8aebfbe4b53c2e6307f1b595605eed246b/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733", size = 34437, upload-time = "2025-12-16T00:35:19.437Z" }, { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, @@ -1443,13 +1337,6 @@ version = "3.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/6a/33d1702184d94106d3cdd7bfb788e19723206fce152e303473ca3b946c7b/greenlet-3.3.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d", size = 273658, upload-time = "2025-12-04T14:23:37.494Z" }, - { url = "https://files.pythonhosted.org/packages/d6/b7/2b5805bbf1907c26e434f4e448cd8b696a0b71725204fa21a211ff0c04a7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb", size = 574810, upload-time = "2025-12-04T14:50:04.154Z" }, - { url = "https://files.pythonhosted.org/packages/94/38/343242ec12eddf3d8458c73f555c084359883d4ddc674240d9e61ec51fd6/greenlet-3.3.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd", size = 586248, upload-time = "2025-12-04T14:57:39.35Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a8/15d0aa26c0036a15d2659175af00954aaaa5d0d66ba538345bd88013b4d7/greenlet-3.3.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5", size = 586910, upload-time = "2025-12-04T14:25:59.705Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9b/68d5e3b7ccaba3907e5532cf8b9bf16f9ef5056a008f195a367db0ff32db/greenlet-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9", size = 1547206, upload-time = "2025-12-04T15:04:21.027Z" }, - { url = "https://files.pythonhosted.org/packages/66/bd/e3086ccedc61e49f91e2cfb5ffad9d8d62e5dc85e512a6200f096875b60c/greenlet-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d", size = 1613359, upload-time = "2025-12-04T14:27:26.548Z" }, - { url = "https://files.pythonhosted.org/packages/f4/6b/d4e73f5dfa888364bbf02efa85616c6714ae7c631c201349782e5b428925/greenlet-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082", size = 300740, upload-time = "2025-12-04T14:47:52.773Z" }, { url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" }, { url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" }, { url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" }, @@ -1518,16 +1405,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" }, - { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" }, - { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" }, - { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" }, - { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" }, - { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" }, - { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" }, - { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" }, - { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" }, { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, @@ -1664,18 +1541,6 @@ version = "3.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3a/06/b31f040a8764336a11152e474a7abcb3782fedb0d1cdf78f442b82878c56/ijson-3.5.1.tar.gz", hash = "sha256:af40bd1a85f55db0b8b30715c858761306bd92d5590148636f75c3309e6e76bd", size = 69913, upload-time = "2026-07-06T17:37:42.923Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/b8/6401c0e2f99aeff22fc740a1b1c2328269a81050c0c178462d0452e27c7e/ijson-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8b4ed62287feee41b90b55ae2800ef56d6bdfd2fbfa02b4fd0634cd4524bc995", size = 89054, upload-time = "2026-07-06T17:36:03.274Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ad/8d9e1f076560efcc6727b06f3276f30bb811961332d83567de70c179e0e8/ijson-3.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9708c0a3d1f86056049de631933aef8ec57f2008d4cb55ce241790c7ed557428", size = 60674, upload-time = "2026-07-06T17:36:04.326Z" }, - { url = "https://files.pythonhosted.org/packages/ab/e7/8f001e823846c270e0e9c3526ea99dc3b1ba51b9501e060d8337830d6c76/ijson-3.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:904e8cf9ca69f5de5b6bb405a4a075ce3da3413ad50c11f6813f1201e14a8e45", size = 60738, upload-time = "2026-07-06T17:36:05.283Z" }, - { url = "https://files.pythonhosted.org/packages/ae/97/c023067cb5ba4cc455a92110a021863fbe3dc3ffcca34ef95aea9290b8f1/ijson-3.5.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8cb5db5bc122da64efb24ce358752d5e097ab41d224ce2992536a0f9073fe4fd", size = 126651, upload-time = "2026-07-06T17:36:06.211Z" }, - { url = "https://files.pythonhosted.org/packages/a6/93/7c2207377b40bc1227c8fe1811e080f3b73cd4a9486af9c1166486c3156c/ijson-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cae04eff4006fc36bf0b030b38e2646a97092d87d933d20cfe7262e26ed32321", size = 133200, upload-time = "2026-07-06T17:36:07.239Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ea/e4d3f64822fb29d54970909e1e2784daa17f75fe3c6c27544fe92e247aad/ijson-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70542d4542f079c394e525559188d69e3ccfbfd9bab899acd0bf1dbc7323ddd5", size = 130361, upload-time = "2026-07-06T17:36:08.332Z" }, - { url = "https://files.pythonhosted.org/packages/03/77/a61b6b68868a7368a0e4335975c5352e6c354d05eb73dbef19e796b3eaab/ijson-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1321495807dcdaca002cb45f24033208ce1d9f5ffc0c5a5584c5f466d0dcbbd5", size = 133618, upload-time = "2026-07-06T17:36:09.41Z" }, - { url = "https://files.pythonhosted.org/packages/a5/0c/05bde03ef651ae2e1033f136c56f7f5565e9f53e7ff91ca83bfd581cbafa/ijson-3.5.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9fac9284d62c4317d541274e15a6a6ab6f6d22561579f6570967e3a6eaafaebc", size = 128554, upload-time = "2026-07-06T17:36:10.464Z" }, - { url = "https://files.pythonhosted.org/packages/41/42/29bb5561c60e1f9d58d4fbef686e35b9440d9b56f9254c1c70b807c8f649/ijson-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1be3a586c8821ecab9ea8b256f39305c8a0cc33222fe393bcc1fb9221470732b", size = 131233, upload-time = "2026-07-06T17:36:11.783Z" }, - { url = "https://files.pythonhosted.org/packages/69/f7/b0176baac5129b79aa366161d5f524ead91b901f16a5020e495c3f83bcc5/ijson-3.5.1-cp310-cp310-win32.whl", hash = "sha256:3ab6378d9c19f01f206f27f762837ad3979330cabd7864e1b17934c03de6056c", size = 52221, upload-time = "2026-07-06T17:36:12.806Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ef/a707b5830722e9f7af347945f9ee0f360d38922366bc1400c6177154eb9c/ijson-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:0663f718c6123899c6bfd9c449ec195cd8c67666b7ea2c7b36fa0cc0dcb13e17", size = 54641, upload-time = "2026-07-06T17:36:13.724Z" }, - { url = "https://files.pythonhosted.org/packages/bb/6b/834e7a4ec7e1019b596daf8d74f697aa1d3e38a17a9c31af6081c070557b/ijson-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:0a682954b60fcd0c23d504df6fb1ebde051305e41c9b350f39a3b8bfb168def7", size = 53954, upload-time = "2026-07-06T17:36:14.718Z" }, { url = "https://files.pythonhosted.org/packages/97/d3/16d1595d3ef4743fc55129211bc52f52d59c582d0b7be045d8c04be0ae0c/ijson-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2aa9d0cf21d4de89fb633e5ec27e9ad02c3f9a4ffa3940d120b23b8aed3acffc", size = 89069, upload-time = "2026-07-06T17:36:15.727Z" }, { url = "https://files.pythonhosted.org/packages/32/a5/ddba126e2d46cf3b86ad762aeb5e0a02ce0ebc6e4529fe7d06eecb217844/ijson-3.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05eba5268a38809ba1c3dbfa44ea67336e2c353fc11768acc9c6442fe0ccac50", size = 60697, upload-time = "2026-07-06T17:36:16.66Z" }, { url = "https://files.pythonhosted.org/packages/dc/74/444d8d00a4506a79fc5544614106fa48d5f6f7049511148d8b6cddb8e9d7/ijson-3.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:40ddd236c80a667dd6a1f6b625d18ddac68b8719ff795761b7542f2e1f78e4a4", size = 60747, upload-time = "2026-07-06T17:36:17.927Z" }, @@ -1813,18 +1678,6 @@ version = "0.12.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" }, - { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" }, - { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" }, - { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" }, - { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" }, - { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" }, - { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" }, - { url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368, upload-time = "2025-11-09T20:46:58.638Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847, upload-time = "2025-11-09T20:47:00.295Z" }, { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, @@ -1968,7 +1821,6 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "protobuf" }, { name = "rich" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1e/a4/518b4deb1b6bffc150c5229b209121b76185ecd369a460fc3b740e8e4af0/logfire-4.19.0.tar.gz", hash = "sha256:4ec49f1e6be2512f2c60ade053fd8a7610539dbe570e4ba53df91435ef7db888", size = 634738, upload-time = "2026-01-16T10:11:57.191Z" } @@ -2017,17 +1869,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, @@ -2131,14 +1972,6 @@ version = "0.21.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/38/d591d9f66d43d897ecbd249f2833665823d19c8b043f16619bc8343e23df/msgspec-0.21.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72d9cd03241b8b2edb2e12dcc66c500fa480d8cbd71a8bac105809d468882064", size = 195172, upload-time = "2026-04-12T21:43:45.062Z" }, - { url = "https://files.pythonhosted.org/packages/69/1a/6899188b5982ec1324e0c629b7801eed2db987f6634fab58abd9fc82d317/msgspec-0.21.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed2ab278200e743a1d2610a4e0c8fc74f6cecb8548544cdec43f927bd9265238", size = 188316, upload-time = "2026-04-12T21:43:46.641Z" }, - { url = "https://files.pythonhosted.org/packages/9e/95/7e591b4fa11fdbbf9891164473c23420a8c781ef553295abe416bf335f42/msgspec-0.21.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd677e3001fdfed9186de72eab434da2976303cd5eb9550921d3d0c3e3e168ce", size = 216565, upload-time = "2026-04-12T21:43:48.081Z" }, - { url = "https://files.pythonhosted.org/packages/19/86/714feeaf3b84cf2027235681725593840153dedd2868578f9f2715e296bb/msgspec-0.21.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f667b90b37fad734a91671abd68e0d7f4d066862771b87e91c53996dcb7a9027", size = 222689, upload-time = "2026-04-12T21:43:49.385Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b9/4384243e814f2579e5205e17d170b9c1a30121afd1393298d904817a7fa7/msgspec-0.21.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:49880fd20fdbcfe1b793f07dd83f12572bab679c9800352c8b2240289aa46a06", size = 222343, upload-time = "2026-04-12T21:43:50.612Z" }, - { url = "https://files.pythonhosted.org/packages/04/01/4b227d9c4057346271043632bad41979cf8c3dca372e41bb1f7d546395b2/msgspec-0.21.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae0162e22849a5e91eaad907766525107523b0daea3df267a9fcb5ba4e0936ae", size = 225607, upload-time = "2026-04-12T21:43:52.129Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ce/27021d1c3e5da837743092a7b7a5e8818397e1f4c05ee8b068bd7d1fd78a/msgspec-0.21.1-cp310-cp310-win_amd64.whl", hash = "sha256:f041a2279f31e3a53319005e4d60ba77c085cfcbe394cdc7ce803c2d01fe9449", size = 188392, upload-time = "2026-04-12T21:43:53.384Z" }, - { url = "https://files.pythonhosted.org/packages/80/2b/daf7a8d6d7cf00e0dcd0439178b284ade701234abdcadf3385601da04fbd/msgspec-0.21.1-cp310-cp310-win_arm64.whl", hash = "sha256:1bf17cbd7b28a5dffc7e764c654eed8ccde5e0f1de7970628608304640d4ce4e", size = 174191, upload-time = "2026-04-12T21:43:54.6Z" }, { url = "https://files.pythonhosted.org/packages/ba/7f/bbc4e74cd33d316b75541149e4d35b163b63bce066530ae185a2ec3b5bfc/msgspec-0.21.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b504b6e7f7a22a24b27232b73034421692147865162daaec9f3bf62439007c87", size = 193131, upload-time = "2026-04-12T21:43:56.094Z" }, { url = "https://files.pythonhosted.org/packages/c1/60/504886af1aaf854112663b842d5eea9a15d9588f9bf7d0d2df736424b84d/msgspec-0.21.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4692b7c1609155708c4418f88e92f63c13fdf08aa095c84bae82bad75b53389b", size = 186597, upload-time = "2026-04-12T21:43:57.242Z" }, { url = "https://files.pythonhosted.org/packages/fa/54/d24ddeaa65b5278c9e67f48ce3c17a9831e8f3722f3c8322ee120aca22ef/msgspec-0.21.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3124010b3815451494c85ff345e693cb9fe5889cfcbbef39ed8622e0e72319c", size = 215158, upload-time = "2026-04-12T21:43:58.442Z" }, @@ -2161,29 +1994,8 @@ wheels = [ name = "multidict" version = "6.7.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] 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" }, @@ -2259,59 +2071,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" }, ] -[[package]] -name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, -] - [[package]] name = "numpy" version = "2.4.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, @@ -2513,19 +2276,6 @@ version = "3.11.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" }, - { url = "https://files.pythonhosted.org/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" }, - { url = "https://files.pythonhosted.org/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" }, - { url = "https://files.pythonhosted.org/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" }, - { url = "https://files.pythonhosted.org/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" }, - { url = "https://files.pythonhosted.org/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" }, - { url = "https://files.pythonhosted.org/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" }, - { url = "https://files.pythonhosted.org/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" }, - { url = "https://files.pythonhosted.org/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" }, - { url = "https://files.pythonhosted.org/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" }, - { url = "https://files.pythonhosted.org/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" }, - { url = "https://files.pythonhosted.org/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" }, { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, @@ -2581,21 +2331,13 @@ name = "pandas" version = "2.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827, upload-time = "2024-09-20T13:08:42.347Z" }, - { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897, upload-time = "2024-09-20T13:08:45.807Z" }, - { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908, upload-time = "2024-09-20T18:37:13.513Z" }, - { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210, upload-time = "2024-09-20T13:08:48.325Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292, upload-time = "2024-09-20T19:01:54.443Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379, upload-time = "2024-09-20T13:08:50.882Z" }, - { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471, upload-time = "2024-09-20T13:08:53.332Z" }, { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, @@ -2617,8 +2359,7 @@ name = "pandas-stubs" version = "2.3.3.260113" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "types-pytz" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/5d/be23854a73fda69f1dbdda7bc10fbd6f930bd1fa87aaec389f00c901c1e8/pandas_stubs-2.3.3.260113.tar.gz", hash = "sha256:076e3724bcaa73de78932b012ec64b3010463d377fa63116f4e6850643d93800", size = 116131, upload-time = "2026-01-13T22:30:16.704Z" } @@ -2684,7 +2425,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel" }, { name = "pyyaml" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8f/96/15bce512c8027696b20586b18ac4b239afe8d312dbaa2a0099e8fb8f8424/poethepoet-0.31.1.tar.gz", hash = "sha256:d6b66074edf85daf115bb916eae0afd6387d19e1562e1c9ef7d61d5c585696aa", size = 61520, upload-time = "2024-11-25T21:24:39.365Z" } wheels = [ @@ -2723,21 +2463,6 @@ 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" }, @@ -2824,17 +2549,6 @@ name = "psycopg-binary" version = "3.3.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/d7/edfb0d9e56081246fd88490f99b1bafebd3588480cca601a4de0c41a3e08/psycopg_binary-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0768c5f32934bb52a5df098317eca9bdcf411de627c5dca2ee57662b64b54b41", size = 4597785, upload-time = "2025-12-06T17:31:44.867Z" }, - { url = "https://files.pythonhosted.org/packages/71/45/8458201d9573dd851263a05cefddd4bfd31e8b3c6434b3e38d62aea9f15a/psycopg_binary-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:09b3014013f05cd89828640d3a1db5f829cc24ad8fa81b6e42b2c04685a0c9d4", size = 4664440, upload-time = "2025-12-06T17:31:49.1Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/484260d87456cfe88dc219c1919026f11949b9d1de8a6371ddbe027d4d60/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3789d452a9d17a841c7f4f97bbcba51a21f957ea35641a4c98507520e6b6a068", size = 5478355, upload-time = "2025-12-06T17:31:52.657Z" }, - { url = "https://files.pythonhosted.org/packages/34/b2/18c91630c30c83f534c2bfa75fb533293fc9c3ab31bb7f2bf1cd9579c53b/psycopg_binary-3.3.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44e89938d36acc4495735af70a886d206a5bfdc80258f95b69b52f68b2968d9e", size = 5152398, upload-time = "2025-12-06T17:31:56.092Z" }, - { url = "https://files.pythonhosted.org/packages/c0/14/7c705e1934107196d9dca2040cf34bce2ca26de62520e43073d2673052d4/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ed9da805e52985b0202aed4f352842c907c6b4fc6c7c109c6e646c32e2f43b", size = 6748982, upload-time = "2025-12-06T17:32:00.611Z" }, - { url = "https://files.pythonhosted.org/packages/56/18/80197c47798926f79e563af02a71d1abecab88cf45ddf8dc960700598da7/psycopg_binary-3.3.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3a9ccdfee4ae59cf9bf1822777e763bc097ed208f4901e21537fca1070e1391", size = 4991214, upload-time = "2025-12-06T17:32:03.897Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2e/e88e2f678f5d1a968d87e57b30915061c1157e916b8aaa9b0b78bca95e25/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de9173f8cc0efd88ac2a89b3b6c287a9a0011cdc2f53b2a12c28d6fd55f9f81c", size = 4517421, upload-time = "2025-12-06T17:32:07.287Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/d56813b24370723bcd62bf73871aee4d5fca0536f3476c4c4d5b037e3c7f/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0611f4822674f3269e507a307236efb62ae5a828fcfc923ac85fe22ca19fd7c8", size = 4206124, upload-time = "2025-12-06T17:32:10.374Z" }, - { url = "https://files.pythonhosted.org/packages/91/81/5a11a898969edf0ee43d0613a6dfd689a0aa12d418c69e148a8ff153fbc7/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:522b79c7db547767ca923e441c19b97a2157f2f494272a119c854bba4804e186", size = 3937067, upload-time = "2025-12-06T17:32:13.852Z" }, - { url = "https://files.pythonhosted.org/packages/a1/33/a6180ff1e747a0395876d985e8e295c9d7cbe956a2d66f165e7c67cffe55/psycopg_binary-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ea41c0229f3f5a3844ad0857a83a9f869aa7b840448fa0c200e6bcf85d33d19", size = 4243731, upload-time = "2025-12-06T17:32:16.803Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5b/9c1b6fbc900d5b525946ed9a477865c5016a5306080c0557248bb04f1a5b/psycopg_binary-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:8ea05b499278790a8fa0ff9854ab0de2542aca02d661ddff94e830df971ff640", size = 3546403, upload-time = "2025-12-06T17:32:19.621Z" }, { url = "https://files.pythonhosted.org/packages/57/d9/49640360fc090d27afc4655021544aa71d5393ebae124ffa53a04474b493/psycopg_binary-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94503b79f7da0b65c80d0dbb2f81dd78b300319ec2435d5e6dcf9622160bc2fa", size = 4597890, upload-time = "2025-12-06T17:32:23.087Z" }, { url = "https://files.pythonhosted.org/packages/85/cf/99634bbccc8af0dd86df4bce705eea5540d06bb7f5ab3067446ae9ffdae4/psycopg_binary-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07a5f030e0902ec3e27d0506ceb01238c0aecbc73ecd7fa0ee55f86134600b5b", size = 4664396, upload-time = "2025-12-06T17:32:26.421Z" }, { url = "https://files.pythonhosted.org/packages/40/db/6035dff6d5c6dfca3a4ab0d2ac62ede623646e327e9f99e21e0cf08976c6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e09d0d93d35c134704a2cb2b15f81ffc8174fd602f3e08f7b1a3d8896156cf0", size = 5478743, upload-time = "2025-12-06T17:32:29.901Z" }, @@ -2877,17 +2591,6 @@ version = "2.9.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/f2/8e377d29c2ecf99f6062d35ea606b036e8800720eccfec5fe3dd672c2b24/psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", size = 3756506, upload-time = "2025-10-10T11:10:30.144Z" }, - { url = "https://files.pythonhosted.org/packages/24/cc/dc143ea88e4ec9d386106cac05023b69668bd0be20794c613446eaefafe5/psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", size = 3863943, upload-time = "2025-10-10T11:10:34.586Z" }, - { url = "https://files.pythonhosted.org/packages/8c/df/16848771155e7c419c60afeb24950b8aaa3ab09c0a091ec3ccca26a574d0/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", size = 4410873, upload-time = "2025-10-10T11:10:38.951Z" }, - { url = "https://files.pythonhosted.org/packages/43/79/5ef5f32621abd5a541b89b04231fe959a9b327c874a1d41156041c75494b/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", size = 4468016, upload-time = "2025-10-10T11:10:43.319Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9b/d7542d0f7ad78f57385971f426704776d7b310f5219ed58da5d605b1892e/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", size = 4164996, upload-time = "2025-10-10T11:10:46.705Z" }, - { url = "https://files.pythonhosted.org/packages/14/ed/e409388b537fa7414330687936917c522f6a77a13474e4238219fcfd9a84/psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", size = 3981881, upload-time = "2025-10-30T02:54:57.182Z" }, - { url = "https://files.pythonhosted.org/packages/bf/30/50e330e63bb05efc6fa7c1447df3e08954894025ca3dcb396ecc6739bc26/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", size = 3650857, upload-time = "2025-10-10T11:10:50.112Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e0/4026e4c12bb49dd028756c5b0bc4c572319f2d8f1c9008e0dad8cc9addd7/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", size = 3296063, upload-time = "2025-10-10T11:10:54.089Z" }, - { url = "https://files.pythonhosted.org/packages/2c/34/eb172be293c886fef5299fe5c3fcf180a05478be89856067881007934a7c/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", size = 3043464, upload-time = "2025-10-30T02:55:02.483Z" }, - { url = "https://files.pythonhosted.org/packages/18/1c/532c5d2cb11986372f14b798a95f2eaafe5779334f6a80589a68b5fcf769/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", size = 3345378, upload-time = "2025-10-10T11:11:01.039Z" }, - { url = "https://files.pythonhosted.org/packages/70/e7/de420e1cf16f838e1fa17b1120e83afff374c7c0130d088dba6286fcf8ea/psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", size = 2713904, upload-time = "2025-10-10T11:11:04.81Z" }, { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" }, { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" }, { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, @@ -2943,13 +2646,6 @@ version = "21.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d9/110de31880016e2afc52d8580b397dbe47615defbf09ca8cf55f56c62165/pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26", size = 31196837, upload-time = "2025-07-18T00:54:34.755Z" }, - { url = "https://files.pythonhosted.org/packages/df/5f/c1c1997613abf24fceb087e79432d24c19bc6f7259cab57c2c8e5e545fab/pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79", size = 32659470, upload-time = "2025-07-18T00:54:38.329Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ed/b1589a777816ee33ba123ba1e4f8f02243a844fed0deec97bde9fb21a5cf/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb", size = 41055619, upload-time = "2025-07-18T00:54:42.172Z" }, - { url = "https://files.pythonhosted.org/packages/44/28/b6672962639e85dc0ac36f71ab3a8f5f38e01b51343d7aa372a6b56fa3f3/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51", size = 42733488, upload-time = "2025-07-18T00:54:47.132Z" }, - { url = "https://files.pythonhosted.org/packages/f8/cc/de02c3614874b9089c94eac093f90ca5dfa6d5afe45de3ba847fd950fdf1/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a", size = 43329159, upload-time = "2025-07-18T00:54:51.686Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594", size = 45050567, upload-time = "2025-07-18T00:54:56.679Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634", size = 26217959, upload-time = "2025-07-18T00:55:00.482Z" }, { url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234, upload-time = "2025-07-18T00:55:03.812Z" }, { url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370, upload-time = "2025-07-18T00:55:07.495Z" }, { url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424, upload-time = "2025-07-18T00:55:11.461Z" }, @@ -3033,7 +2729,6 @@ name = "pydantic-ai-slim" version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "genai-prices" }, { name = "griffe" }, { name = "httpx" }, @@ -3118,19 +2813,6 @@ dependencies = [ ] 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" }, @@ -3167,14 +2849,6 @@ wheels = [ { 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" }, @@ -3307,12 +2981,10 @@ version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] 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 = [ @@ -3409,9 +3081,6 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, - { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, @@ -3435,15 +3104,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, @@ -3471,17 +3131,6 @@ version = "3.14.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/d1/0efa42a602ed466d3ca1c462eed5d62015c3fd2a402199e2c4b87aa5aa25/rapidfuzz-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9fcd4d751a4fffa17aed1dde41647923c72c74af02459ad1222e3b0022da3a1", size = 1952376, upload-time = "2025-11-01T11:52:29.175Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/37a169bb28b23850a164e6624b1eb299e1ad73c9e7c218ee15744e68d628/rapidfuzz-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ad73afb688b36864a8d9b7344a9cf6da186c471e5790cbf541a635ee0f457f2", size = 1390903, upload-time = "2025-11-01T11:52:31.239Z" }, - { url = "https://files.pythonhosted.org/packages/3c/91/b37207cbbdb6eaafac3da3f55ea85287b27745cb416e75e15769b7d8abe8/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5fb2d978a601820d2cfd111e2c221a9a7bfdf84b41a3ccbb96ceef29f2f1ac7", size = 1385655, upload-time = "2025-11-01T11:52:32.852Z" }, - { url = "https://files.pythonhosted.org/packages/f2/bb/ca53e518acf43430be61f23b9c5987bd1e01e74fcb7a9ee63e00f597aefb/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d83b8b712fa37e06d59f29a4b49e2e9e8635e908fbc21552fe4d1163db9d2a1", size = 3164708, upload-time = "2025-11-01T11:52:34.618Z" }, - { url = "https://files.pythonhosted.org/packages/df/e1/7667bf2db3e52adb13cb933dd4a6a2efc66045d26fa150fc0feb64c26d61/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:dc8c07801df5206b81ed6bd6c35cb520cf9b6c64b9b0d19d699f8633dc942897", size = 1221106, upload-time = "2025-11-01T11:52:36.069Z" }, - { url = "https://files.pythonhosted.org/packages/05/8a/84d9f2d46a2c8eb2ccae81747c4901fa10fe4010aade2d57ce7b4b8e02ec/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c71ce6d4231e5ef2e33caa952bfe671cb9fd42e2afb11952df9fad41d5c821f9", size = 2406048, upload-time = "2025-11-01T11:52:37.936Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a9/a0b7b7a1b81a020c034eb67c8e23b7e49f920004e295378de3046b0d99e1/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0e38828d1381a0cceb8a4831212b2f673d46f5129a1897b0451c883eaf4a1747", size = 2527020, upload-time = "2025-11-01T11:52:39.657Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bc/416df7d108b99b4942ba04dd4cf73c45c3aadb3ef003d95cad78b1d12eb9/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da2a007434323904719158e50f3076a4dadb176ce43df28ed14610c773cc9825", size = 4273958, upload-time = "2025-11-01T11:52:41.017Z" }, - { url = "https://files.pythonhosted.org/packages/81/d0/b81e041c17cd475002114e0ab8800e4305e60837882cb376a621e520d70f/rapidfuzz-3.14.3-cp310-cp310-win32.whl", hash = "sha256:fce3152f94afcfd12f3dd8cf51e48fa606e3cb56719bccebe3b401f43d0714f9", size = 1725043, upload-time = "2025-11-01T11:52:42.465Z" }, - { url = "https://files.pythonhosted.org/packages/09/6b/64ad573337d81d64bc78a6a1df53a72a71d54d43d276ce0662c2e95a1f35/rapidfuzz-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:37d3c653af15cd88592633e942f5407cb4c64184efab163c40fcebad05f25141", size = 1542273, upload-time = "2025-11-01T11:52:44.005Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5e/faf76e259bc15808bc0b86028f510215c3d755b6c3a3911113079485e561/rapidfuzz-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:cc594bbcd3c62f647dfac66800f307beaee56b22aaba1c005e9c4c40ed733923", size = 814875, upload-time = "2025-11-01T11:52:45.405Z" }, { url = "https://files.pythonhosted.org/packages/76/25/5b0a33ad3332ee1213068c66f7c14e9e221be90bab434f0cb4defa9d6660/rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d", size = 1953885, upload-time = "2025-11-01T11:52:47.75Z" }, { url = "https://files.pythonhosted.org/packages/2d/ab/f1181f500c32c8fcf7c966f5920c7e56b9b1d03193386d19c956505c312d/rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3", size = 1390200, upload-time = "2025-11-01T11:52:49.491Z" }, { url = "https://files.pythonhosted.org/packages/14/2a/0f2de974ececad873865c6bb3ea3ad07c976ac293d5025b2d73325aac1d4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850", size = 1389319, upload-time = "2025-11-01T11:52:51.224Z" }, @@ -3531,23 +3180,6 @@ version = "2026.1.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, - { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, - { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, - { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, - { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, - { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, - { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, - { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, - { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, - { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, - { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, - { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, @@ -3647,7 +3279,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } wheels = [ @@ -3662,7 +3293,6 @@ dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "rich" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6b/d1/b60ca6a8745e76800b50c7ee246fd73f08a3be5d8e0b551fc93c19fa1203/rich_click-1.9.5.tar.gz", hash = "sha256:48120531493f1533828da80e13e839d471979ec8d7d0ca7b35f86a1379cc74b6", size = 73927, upload-time = "2025-12-21T14:49:44.167Z" } wheels = [ @@ -3688,20 +3318,6 @@ version = "0.30.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, @@ -3846,14 +3462,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/2f/7a/610a53124c3d7e5b643f5670cdabf26c46d14cd16ff0a9c934e7c3255aa1/serpyco_rs-1.18.0.tar.gz", hash = "sha256:0ba9fdd47c699392dfafa259cce2e492be0f9ddaf63ecafd7df6821ceead25b0", size = 83255, upload-time = "2026-01-16T16:03:36.179Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/bd/eab7a82360fb435896aecf21bd9af4385fd95c3532d9fb4507b5c06e6798/serpyco_rs-1.18.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:019b1d2e62907abf9ee0d1f0618ed240a278ccdea3a9a58ab7bf949a8cdb4207", size = 926828, upload-time = "2026-01-16T16:02:34.955Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/d8dfe90c7eaab2049129d1e2e0c63e09d0ce33292d6b4090bed9043c75eb/serpyco_rs-1.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7771feb7f0b501ac2334b2504081474dde426d0b89c8c06a64a28c1a09f67994", size = 498290, upload-time = "2026-01-16T16:02:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/03/b9/14c037b89b04d6dbc8c26a78c6b952bdbd7a8c5fdb36ba20bc877f5c5bed/serpyco_rs-1.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2bc65dd85b093e337836b5ece0ba64bbd82a8eb1e009975935180ec92cf35e1", size = 519427, upload-time = "2026-01-16T16:02:38.106Z" }, - { url = "https://files.pythonhosted.org/packages/e4/03/2487df265b8c5c252f9f3348c02e4f4127c429999829fe5d746082f38079/serpyco_rs-1.18.0-cp310-cp310-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:360cb49e6ba321edb83ddc09b59f4b92080ba50e0862740281e08f3cc066b91c", size = 554277, upload-time = "2026-01-16T16:02:39.191Z" }, - { url = "https://files.pythonhosted.org/packages/2d/b8/c4f42b45584e6cb80442b0702ca8f99418fe4c63c4f87a62804f4a4196e3/serpyco_rs-1.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2acd2bce785558fdc413eeccfb703edd9606079479e1e7dce4aa158473cb045", size = 542170, upload-time = "2026-01-16T16:02:40.546Z" }, - { url = "https://files.pythonhosted.org/packages/bf/09/aaf4bb036c1abbf4f892e6a78d82139497f46b842eb62451a03902072d54/serpyco_rs-1.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34859e2e2fd16a7d046a7997fb8a299921ec4d31c611a82f133a3f5aa4a5b8c5", size = 538870, upload-time = "2026-01-16T16:02:41.806Z" }, - { url = "https://files.pythonhosted.org/packages/d7/56/41e5ec8842b6b1e89761025ef79a1c596a34ad2aec70862ee10a5b817f6a/serpyco_rs-1.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1c343fa2b74a91fb8f9784437b3c5fa9e22a1eb36b7401f2a1c02879576a01", size = 496532, upload-time = "2026-01-16T16:02:42.894Z" }, - { url = "https://files.pythonhosted.org/packages/23/63/162290fc5a2b7b1e943045b8db18b59325f6cdf53da7b0a8016d1fcb6e7b/serpyco_rs-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:22f16a0fb2b352840557d0ef77c82c095ff7ce5e5875a8a7ac3c6aaf905070a9", size = 365646, upload-time = "2026-01-16T16:02:43.963Z" }, { url = "https://files.pythonhosted.org/packages/f4/33/3629a32c4aec96e9b04403d933f6fba21c5857c8c2423d10aa1b9f326253/serpyco_rs-1.18.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9a21b9d22af48c8b88f0ad1a4857603242c2d7b87bfe1590ae6a2b72503fdf55", size = 924429, upload-time = "2026-01-16T16:02:45.063Z" }, { url = "https://files.pythonhosted.org/packages/9b/4e/c57a015ab307e7237e386930774399abb11afed26393e008340e956e3cd5/serpyco_rs-1.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7ca292c49f60f80ced747278b505d7b7bd7b481cbefd5339192769090e975e9", size = 498133, upload-time = "2026-01-16T16:02:46.537Z" }, { url = "https://files.pythonhosted.org/packages/6f/73/8d27e6e4e8ddc1aaa2decad35a2de6de6b3a2a1babd177e84db7f6c4306c/serpyco_rs-1.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:846e397fa8f58372eea29ab439d7b86174b713e6039aa8ee28cc61da1400fb5f", size = 519097, upload-time = "2026-01-16T16:02:47.937Z" }, @@ -3925,11 +3533,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/25/df/41fe26b68801e3d59653a5dc7ce87a92e9d967dcad7b59b035b8c9804815/snowflake_connector_python-3.18.0.tar.gz", hash = "sha256:41a46eb9824574c5f8068e3ed5c02a2dc0a733ed08ee81fa1fb3dd0ebe921728", size = 798019, upload-time = "2025-10-06T12:15:34.301Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/66/2be9bfebaad12f8b0fbeee68542f14b7a37801b991e3f99adab98ca235ff/snowflake_connector_python-3.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e17a9e806823d3a0e578cf9349f6a93810a582b3132903ea9e1683854d08da00", size = 1014396, upload-time = "2025-10-06T12:15:36.069Z" }, - { url = "https://files.pythonhosted.org/packages/8e/38/e00f81687b56a9419c2b0de3adcab449fc1e7d7a5383c29835148b1bb311/snowflake_connector_python-3.18.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:1841b60dc376639493dfc520cf39ad4f4da1f30286bba57e878d57414263d628", size = 1027175, upload-time = "2025-10-06T12:15:37.632Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ae/f45696a00e63fad3e153c01b8fe5c2d55aba954bb69bd09c7d2d0a290cba/snowflake_connector_python-3.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d37263dd288abb649820b7e34af96dc6b2d2115bf5521a2526245f81ddb0cb", size = 2650237, upload-time = "2025-10-06T12:15:14.24Z" }, - { url = "https://files.pythonhosted.org/packages/c1/dd/843ac8067efb061f66c4e186c293270b887103b162a73559660b4fb0d31e/snowflake_connector_python-3.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fb9fc9d8c2c7d209ba89282d367a32e75b0688afd4a3f02409e24f153c1a32e", size = 2678195, upload-time = "2025-10-06T12:15:16.975Z" }, - { url = "https://files.pythonhosted.org/packages/e8/b2/035e0e316f875f4410d880e12a2765063c054e12e0184a3d86f2728818e5/snowflake_connector_python-3.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfa6b234f53ec624149e21156d0a98e43408d194f2e65bcfaf30acefd35a581e", size = 1161494, upload-time = "2025-10-06T12:15:51.363Z" }, { url = "https://files.pythonhosted.org/packages/87/7e/b932b9897ea7e83b2184443c5222af2f71526e8bce91ecd64ac20b87527c/snowflake_connector_python-3.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5fcb9a25a9b77b6cd86dfc6a6324b9910e15a493a916983229011ce3509b5f", size = 1014589, upload-time = "2025-10-06T12:15:39.26Z" }, { url = "https://files.pythonhosted.org/packages/7e/79/97f777d3d26392901910e27f0d41e9a6dc72fba40cb2499febfca7e51e81/snowflake_connector_python-3.18.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d89f608fde2fb0597ca5e020c4ac602027dc67f11b61b4d1e5448163bae4edc", size = 1027163, upload-time = "2025-10-06T12:15:40.651Z" }, { url = "https://files.pythonhosted.org/packages/d9/9f/553f9a2ec3ce4ab960c8d3611ecbd2e66f972841ef1e037dcbcd5148abae/snowflake_connector_python-3.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1afbd9e21180d2b4a76500ac2978b11865fdb3230609f2a9d80ba459fc27f2e4", size = 2661951, upload-time = "2025-10-06T12:15:18.676Z" }, @@ -3974,14 +3577,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/4e/985f7da36f09592c5ade99321c72c15101d23c0bb7eecfd1daaca5714422/sqlalchemy-2.0.43-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069", size = 2133162, upload-time = "2025-08-11T15:52:17.854Z" }, - { url = "https://files.pythonhosted.org/packages/37/34/798af8db3cae069461e3bc0898a1610dc469386a97048471d364dc8aae1c/sqlalchemy-2.0.43-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154", size = 2123082, upload-time = "2025-08-11T15:52:19.181Z" }, - { url = "https://files.pythonhosted.org/packages/fb/0f/79cf4d9dad42f61ec5af1e022c92f66c2d110b93bb1dc9b033892971abfa/sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612", size = 3208871, upload-time = "2025-08-11T15:50:30.656Z" }, - { url = "https://files.pythonhosted.org/packages/56/b3/59befa58fb0e1a9802c87df02344548e6d007e77e87e6084e2131c29e033/sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019", size = 3209583, upload-time = "2025-08-11T15:57:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/29/d2/124b50c0eb8146e8f0fe16d01026c1a073844f0b454436d8544fe9b33bd7/sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20", size = 3148177, upload-time = "2025-08-11T15:50:32.078Z" }, - { url = "https://files.pythonhosted.org/packages/83/f5/e369cd46aa84278107624617034a5825fedfc5c958b2836310ced4d2eadf/sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18", size = 3172276, upload-time = "2025-08-11T15:57:49.477Z" }, - { url = "https://files.pythonhosted.org/packages/de/2b/4602bf4c3477fa4c837c9774e6dd22e0389fc52310c4c4dfb7e7ba05e90d/sqlalchemy-2.0.43-cp310-cp310-win32.whl", hash = "sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00", size = 2101491, upload-time = "2025-08-11T15:54:59.191Z" }, - { url = "https://files.pythonhosted.org/packages/38/2d/bfc6b6143adef553a08295490ddc52607ee435b9c751c714620c1b3dd44d/sqlalchemy-2.0.43-cp310-cp310-win_amd64.whl", hash = "sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b", size = 2125148, upload-time = "2025-08-11T15:55:00.593Z" }, { url = "https://files.pythonhosted.org/packages/9d/77/fa7189fe44114658002566c6fe443d3ed0ec1fa782feb72af6ef7fbe98e7/sqlalchemy-2.0.43-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29", size = 2136472, upload-time = "2025-08-11T15:52:21.789Z" }, { url = "https://files.pythonhosted.org/packages/99/ea/92ac27f2fbc2e6c1766bb807084ca455265707e041ba027c09c17d697867/sqlalchemy-2.0.43-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631", size = 2126535, upload-time = "2025-08-11T15:52:23.109Z" }, { url = "https://files.pythonhosted.org/packages/94/12/536ede80163e295dc57fff69724caf68f91bb40578b6ac6583a293534849/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685", size = 3297521, upload-time = "2025-08-11T15:50:33.536Z" }, @@ -4071,7 +3666,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nexus-rpc" }, { name = "protobuf" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, { name = "types-protobuf" }, { name = "typing-extensions" }, ] @@ -4103,13 +3697,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, - { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, - { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, - { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, @@ -4150,10 +3737,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, - { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, - { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, - { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, ] [[package]] @@ -4400,7 +3983,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } wheels = [ @@ -4416,13 +3998,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f8/11/3e42af9884046efd7c4b08f9e84d4c8b13953ba4761e67e39a95239ed02c/viztracer-1.0.4.tar.gz", hash = "sha256:e8da6faa73d68911d93b676fa2c94bfa20966e23b529e3cbd1c6816f7de83cd7", size = 10213794, upload-time = "2025-05-09T22:38:01.663Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/5c/7298040e0524807d536d85427e1dcdef7413f17c3d918f49d2f37c37140e/viztracer-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe6cb0ffb708c5348f3979034e3db73f213de6c57a8c4f84bb63f396b49f5f6e", size = 10266288, upload-time = "2025-05-09T22:38:53.389Z" }, - { url = "https://files.pythonhosted.org/packages/5b/8c/c138f3d49513635191f8c024a4b57d4312a6f7a1ab656f97fc994f14a914/viztracer-1.0.4-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:1b67e1d0d895230f8c259c242e63e1356b89309f9efca303f6012774bc4a5c81", size = 10265521, upload-time = "2025-05-09T22:42:15.94Z" }, - { url = "https://files.pythonhosted.org/packages/32/63/c93093fa606528121eccc21e5f6be22b4c858766cfa4562a5c8a102102b9/viztracer-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56232f73b75b47ca341f008e87d1d595aa55a148f21beb046eec93162fcf1007", size = 10380151, upload-time = "2025-05-09T22:46:27.733Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ba/b855cc9cb6c8551b6ec28b36adf3b772b5318db32e8441282467414f9e41/viztracer-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b0b024e8f297227b30def56218fad05fb5bbdfcc4f2edeac9ae0993f457bc3", size = 10375023, upload-time = "2025-05-09T22:40:29.978Z" }, - { url = "https://files.pythonhosted.org/packages/4c/0e/8e2c2ffde5bf9c883ae6803156f0da68a4a2837b1370247ae4bb92977b82/viztracer-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:219f028017a5860895e014142f0ab3b4bd043739cb3e9b542da80d39abdf91ce", size = 10384941, upload-time = "2025-05-09T22:40:32.275Z" }, - { url = "https://files.pythonhosted.org/packages/a8/00/b94211550ff4cc437a88aca75e70ae83e9ea12e0cdaddcdbaa4a53befaaf/viztracer-1.0.4-cp310-cp310-win32.whl", hash = "sha256:e7d9fa05c9f0c6ef9bcfe8aab8402157f1c88d3fb2ad543e88a3fc1088a36e7b", size = 10428471, upload-time = "2025-05-09T22:45:47.414Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/1348c9dd474eda5c4b5432658d74b29db8b5d65162734e54e5f008181423/viztracer-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:110ec2999ca1bc0c602d18b414ccb45dbf02c446249b352dc79607f8363fe72b", size = 10431189, upload-time = "2025-05-09T22:45:45.289Z" }, { url = "https://files.pythonhosted.org/packages/ae/0e/e5d6ecef30133b66bee621d5a64086de13ffe70eeb6cf242af3250f55d1c/viztracer-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:477d65c8168e987277ae7e615b6b02f48253d7353003ebc86d64982254fe79c0", size = 10266063, upload-time = "2025-05-09T22:38:55.621Z" }, { url = "https://files.pythonhosted.org/packages/7f/6f/f9a54081de28d51adf34cef98e9771516c661d466597bf69c6a60f08f9e4/viztracer-1.0.4-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:c98d4a6c5021a275c242dca09e056648f5e3c11f8e465e213e588baacece9c72", size = 10265350, upload-time = "2025-05-09T22:42:19.162Z" }, { url = "https://files.pythonhosted.org/packages/9e/6b/1ac927023f81221ed2ff342a02bf4d5e856227059db0100f940882fe955f/viztracer-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c42839895be77d0987230ca1fc10ba18b5a01e30ace791cf723e7b4b4e280b98", size = 10378892, upload-time = "2025-05-09T22:46:31.08Z" }, @@ -4448,18 +4023,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, - { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, - { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, - { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, - { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, - { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, - { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, - { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, @@ -4486,10 +4049,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, - { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, @@ -4523,17 +4082,6 @@ version = "15.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, - { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, - { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, - { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, @@ -4556,12 +4104,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, - { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, - { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, - { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, - { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] @@ -4574,20 +4116,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/4d/67/cfc23dfe54ced1e4388826b29db9b9ab2c70a342b33b7e92cf15866f35a6/whenever-0.8.10.tar.gz", hash = "sha256:5e2a3da71527e299f98eec5bb38c4e79d9527a127107387456125005884fb235", size = 240223, upload-time = "2025-10-16T20:31:23.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/22/b7db5ebe8b74f60f8dc2af8264adac158dbd7d199731509299c3345f7874/whenever-0.8.10-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d9ecb6b649cb7e5c85742f626ddd56d5cf5d276c632a47ec5d72714350300564", size = 390187, upload-time = "2025-10-16T20:30:54.648Z" }, - { url = "https://files.pythonhosted.org/packages/10/65/ed912e437c68e83a3f22477c9f3c67f1810228b8d6566a999f12921025d1/whenever-0.8.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0698cbd2209413f7a0cb84507405587e7b3995ce22504e50477a1a65ec3b65b9", size = 375020, upload-time = "2025-10-16T20:30:45.408Z" }, - { url = "https://files.pythonhosted.org/packages/17/a9/a3e7a9731af8c350e605eff2d2d03d2517a839767efd308337498410d32b/whenever-0.8.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30b2f25ee740f5d201f643982c50f0d6ba2fdbb69704630467d85286e290fdab", size = 397139, upload-time = "2025-10-16T20:29:17.616Z" }, - { url = "https://files.pythonhosted.org/packages/7c/32/8568934b927a122582a4e5551cbc86b9c32c781a5abe32c4697aead88fa4/whenever-0.8.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb6abd25e03e1aaa9c4ab949c1b02d755be6ea2f18d6a86e0d024a66705beec6", size = 436865, upload-time = "2025-10-16T20:29:35.073Z" }, - { url = "https://files.pythonhosted.org/packages/e5/3b/c3765b975fb6ceb0ded61d60a815c2045c1ec2f1e70f1afc2df8645175a1/whenever-0.8.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:228860bfc14e63b7c2c6980e41dee7f4efb397accc06eabc51e9dfeaf633ad5a", size = 430843, upload-time = "2025-10-16T20:29:51.467Z" }, - { url = "https://files.pythonhosted.org/packages/dc/76/b809b29be1b11868275c6e50e23f1b387b9a40953e717a87ec33dd83bfa9/whenever-0.8.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0af24862ded1dcb71e096e7570e6e031f934e7cfa57123363ef21049f8f9fdd4", size = 450953, upload-time = "2025-10-16T20:29:59.761Z" }, - { url = "https://files.pythonhosted.org/packages/2f/c9/b9f2655e6715e2314b25e722eff7de92e75795001bdc85fed6cf9edfbd8d/whenever-0.8.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6331ebf85dd234d33fdd627146f20808c6eb39f8056dbd09715055f21cd7c494", size = 412096, upload-time = "2025-10-16T20:30:27.719Z" }, - { url = "https://files.pythonhosted.org/packages/e7/6f/773ad493573fcddd89d5080589ca334335c07e6ce70c7aef743c66a05067/whenever-0.8.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ce5dfa7769444e12ae8f0fba8bdce05a8081e1829a9de68d4cc02a11ff71131", size = 451054, upload-time = "2025-10-16T20:30:08.951Z" }, - { url = "https://files.pythonhosted.org/packages/1c/54/3558f92753e2356558973980c31c6e118330c354caecaa0144d1690a6854/whenever-0.8.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9768562c5a871b2a6377697eb76943fd798c663a4a96b499e4d2fa69c42d7397", size = 575509, upload-time = "2025-10-16T20:29:26.622Z" }, - { url = "https://files.pythonhosted.org/packages/27/b7/f96f250bb9f420b8c42b265046f3623f6a9ffda9d469fe27f9027895c407/whenever-0.8.10-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f88d9ec50f2dfa4981924cb87fb287708ccb5f770fd93dd9c6fc27641e686c1c", size = 701546, upload-time = "2025-10-16T20:29:42.776Z" }, - { url = "https://files.pythonhosted.org/packages/84/8f/351d01a0aca6c579c5253555d999760f351b6c06cfe3e7699f0236aa8e79/whenever-0.8.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:507462b0f02d7d4cdfe90888a0158ee3d6c5d49fa3ddcd1b44901c6778fd7381", size = 625427, upload-time = "2025-10-16T20:30:17.872Z" }, - { url = "https://files.pythonhosted.org/packages/1f/60/f4e5d3ae33cda28dc30b21fdf494372d2eaeec7f72f458524e7dcc08301a/whenever-0.8.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ba2d930b5e428e1b0c01ef6c8af14eb94f84792c37d79352f954cd9ea791838e", size = 583161, upload-time = "2025-10-16T20:30:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/15/72/e47c7a6ceb2743f65e825121b1a3f95267a865b58a6c8eda310a863ef358/whenever-0.8.10-cp310-cp310-win32.whl", hash = "sha256:b598be861fd711d2df683d32dbb15d05279e2e932a4c31f2f7bfd28196985662", size = 328384, upload-time = "2025-10-16T20:31:03.569Z" }, - { url = "https://files.pythonhosted.org/packages/ac/09/12cd9a390780cd9b31891fd37a18014b6d5c3d2416665f3513a87a291ce1/whenever-0.8.10-cp310-cp310-win_amd64.whl", hash = "sha256:66eab892d56685a84a9d933b8252c68794eede39b5105f20d06b000ff17275d4", size = 320937, upload-time = "2025-10-16T20:31:13.019Z" }, { url = "https://files.pythonhosted.org/packages/39/d1/74854e557ae9f57e410bb4b7e8685a8fb0ec301926ca36e71f6e541ce6ae/whenever-0.8.10-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3f03f9bef7e3bfe40461e74c74af0cf8dc90489dacc2360069faccf2997f4bca", size = 390188, upload-time = "2025-10-16T20:30:56.027Z" }, { url = "https://files.pythonhosted.org/packages/04/fe/dc9d824164824909c230d5c0100332aa62d260fd859cfcfb4f53d119fa79/whenever-0.8.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f42eb10aaf2818b0e26a5d5230c6cb735ca109882ec4b19cb5cf646c0d28120", size = 375020, upload-time = "2025-10-16T20:30:47.115Z" }, { url = "https://files.pythonhosted.org/packages/b2/ff/ea8e043b511a18057b5144ad78206198bf4671e241d5ce4b2338613c478b/whenever-0.8.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b3ddb300e32b19dd9af391d98ba62b21288d628ec17acf4752d96443a3174", size = 397139, upload-time = "2025-10-16T20:29:20.005Z" }, @@ -4625,16 +4153,6 @@ version = "1.17.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, - { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, - { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, - { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, - { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, - { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, - { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, @@ -4678,22 +4196,6 @@ dependencies = [ ] 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" }, From 1b18ffd33094f48ed2448df6f062d815f5262afe Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:00:36 +0000 Subject: [PATCH 02/11] feat(cloud): use generated config API models Co-Authored-By: AJ Steers --- README.md | 2 +- airbyte/_util/api_util.py | 297 +++++++++-------- airbyte/cloud/client.py | 76 +++-- airbyte/cloud/connectors.py | 8 +- airbyte/cloud/organizations.py | 33 +- airbyte/cloud/sync_results.py | 16 +- airbyte/cloud/workspaces.py | 8 +- tests/unit_tests/test_cloud_api_util.py | 352 +++++++++++---------- tests/unit_tests/test_cloud_credentials.py | 238 ++++++++++---- 9 files changed, 606 insertions(+), 424 deletions(-) diff --git a/README.md b/README.md index 08ce821bb..6dbbc8b5e 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ import airbyte as ab source = ab.get_source( "source-faker", - use_python="3.11.17", + use_python="3.11.13", ) ``` diff --git a/airbyte/_util/api_util.py b/airbyte/_util/api_util.py index b28a33092..12e3ff90a 100644 --- a/airbyte/_util/api_util.py +++ b/airbyte/_util/api_util.py @@ -16,12 +16,47 @@ import base64 import json from http import HTTPStatus -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, TypeVar import airbyte_api import requests from airbyte_api import api, models from airbyte_api.errors import SDKError +from airbyte_server_models._config_api import ( + AirbyteCatalog, # noqa: PLC2701 + BuilderProjectForDefinitionRequestBody, # noqa: PLC2701 + BuilderProjectForDefinitionResponse, # noqa: PLC2701 + CheckConnectionRead, # noqa: PLC2701 + ConnectionIdRequestBody, # noqa: PLC2701 + ConnectionState, # noqa: PLC2701 + ConnectionStateCreateOrUpdate, # noqa: PLC2701 + ConnectorBuilderProjectIdWithWorkspaceId, # noqa: PLC2701 + ConnectorBuilderProjectRead, # noqa: PLC2701 + ConnectorBuilderProjectTestingValues, # noqa: PLC2701 + ConnectorBuilderProjectTestingValuesUpdate, # noqa: PLC2701 + DestinationIdRequestBody, # noqa: PLC2701 + ListOrganizationsByUserRequestBody, # noqa: PLC2701 + ListWorkspacesInOrganizationRequestBody, # noqa: PLC2701 + OrganizationIdRequestBody, # noqa: PLC2701 + OrganizationInfoRead, # noqa: PLC2701 + OrganizationRead, + OrganizationReadList, # noqa: PLC2701 + Pagination, # noqa: PLC2701 + PermissionRead, + PermissionReadList, # noqa: PLC2701 + SourceDefinitionSpecification, # noqa: PLC2701 + SourceIdRequestBody, # noqa: PLC2701 + UserAuthIdRequestBody, # noqa: PLC2701 + UserIdRequestBody, # noqa: PLC2701 + UserRead, # noqa: PLC2701 + WebBackendConnectionRead, # noqa: PLC2701 + WebBackendConnectionRequestBody, # noqa: PLC2701 + WebBackendConnectionUpdate, # noqa: PLC2701 + WorkspaceIdRequestBody, # noqa: PLC2701 + WorkspaceRead, + WorkspaceReadList, # noqa: PLC2701 +) +from pydantic import BaseModel, ValidationError from airbyte.constants import CLOUD_API_ROOT, CLOUD_CONFIG_API_ROOT, CLOUD_CONFIG_API_ROOT_ENV_VAR from airbyte.exceptions import ( @@ -49,6 +84,7 @@ JOB_WAIT_TIMEOUT_SECS_DEFAULT = 60 * 60 # 1 hour PAGE_SIZE = 100 JWT_PART_COUNT = 3 +_T = TypeVar("_T", bound=BaseModel) # Job ordering constants for list_jobs API JOB_ORDER_BY_CREATED_AT_DESC = "createdAt|DESC" @@ -1859,12 +1895,13 @@ def _make_config_api_request( *, api_root: str, path: str, - json: dict[str, Any], + request: BaseModel, + response_model: type[_T], client_id: SecretString | None, client_secret: SecretString | None, bearer_token: SecretString | None, config_api_root: str | None = None, -) -> dict[str, Any]: +) -> _T: config_api_root = get_config_api_root(api_root, config_api_root=config_api_root) # Use provided bearer token or generate one from client credentials @@ -1889,7 +1926,7 @@ def _make_config_api_request( method="POST", url=full_url, headers=headers, - json=json, + json=request.model_dump(mode="json", exclude_none=True), ) if not status_ok(response.status_code): try: @@ -1911,7 +1948,17 @@ def _make_config_api_request( }, ) from ex - return response.json() + try: + return response_model.model_validate(response.json()) + except ValidationError as ex: + raise AirbyteError( + message=f"Config API response for {path} did not match the expected schema.", + context={ + "full_url": full_url, + "path": path, + "response": response.text, + }, + ) from ex def check_connector( @@ -1934,18 +1981,26 @@ def check_connector( """ _ = workspace_id # Not used (yet) + request: BaseModel + if connector_type == "source": + request = SourceIdRequestBody(sourceId=actor_id) + else: + request = DestinationIdRequestBody(destinationId=actor_id) + json_result = _make_config_api_request( path=f"/{connector_type}s/check_connection", - json={ - f"{connector_type}Id": actor_id, - }, + request=request, + response_model=CheckConnectionRead, api_root=api_root, config_api_root=config_api_root, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, ) - result, message = json_result.get("status"), json_result.get("message") + result, message = ( + json_result.status.value if json_result.status is not None else None, + json_result.message, + ) if result == "succeeded": return True, None @@ -1957,7 +2012,7 @@ def check_connector( context={ "actor_id": actor_id, "connector_type": connector_type, - "response": json_result, + "response": json_result.model_dump(mode="json", by_alias=True, exclude_none=True), }, ) @@ -2249,7 +2304,7 @@ def get_connector_builder_project_for_definition_id( client_secret: SecretString | None, bearer_token: SecretString | None, config_api_root: str | None = None, -) -> dict[str, Any]: +) -> BuilderProjectForDefinitionResponse: """Get the connector builder project info for a declarative source definition. Uses the Config API endpoint: @@ -2267,15 +2322,16 @@ def get_connector_builder_project_for_definition_id( config_api_root: Optional explicit Config API root URL. Returns: - A dict containing 'builderProjectId' and 'workspaceId' (the workspace that + A response containing 'builderProjectId' and 'workspaceId' (the workspace that owns the builder project, which may differ from the caller's workspace). """ return _make_config_api_request( path="/connector_builder_projects/get_for_definition_id", - json={ - "actorDefinitionId": definition_id, - "workspaceId": workspace_id, - }, + request=BuilderProjectForDefinitionRequestBody( + actorDefinitionId=definition_id, + workspaceId=workspace_id, + ), + response_model=BuilderProjectForDefinitionResponse, api_root=api_root, config_api_root=config_api_root, client_id=client_id, @@ -2293,7 +2349,7 @@ def get_connector_builder_project( client_secret: SecretString | None, bearer_token: SecretString | None, config_api_root: str | None = None, -) -> dict[str, Any]: +) -> ConnectorBuilderProjectRead: """Get a connector builder project, including the draft manifest if one exists. Uses the Config API endpoint: @@ -2311,17 +2367,18 @@ def get_connector_builder_project( config_api_root: Optional explicit Config API root URL. Returns: - A dictionary containing the builder project details. Key fields include: + A generated response containing the builder project details. Key fields include: - builderProject: The project metadata (name, hasDraft, etc.) - declarativeManifest: The draft manifest data (if hasDraft is True), which contains a 'manifest' field with the actual YAML manifest dict. """ return _make_config_api_request( path="/connector_builder_projects/get_with_manifest", - json={ - "workspaceId": workspace_id, - "builderProjectId": builder_project_id, - }, + request=ConnectorBuilderProjectIdWithWorkspaceId( + workspaceId=workspace_id, + builderProjectId=builder_project_id, + ), + response_model=ConnectorBuilderProjectRead, api_root=api_root, config_api_root=config_api_root, client_id=client_id, @@ -2341,7 +2398,7 @@ def update_connector_builder_project_testing_values( # noqa: PLR0913 client_secret: SecretString | None, bearer_token: SecretString | None, config_api_root: str | None = None, -) -> dict[str, Any]: +) -> ConnectorBuilderProjectTestingValues: """Update the testing values for a connector builder project. This call replaces the entire testing values object stored for the project. @@ -2367,12 +2424,13 @@ def update_connector_builder_project_testing_values( # noqa: PLR0913 """ return _make_config_api_request( path="/connector_builder_projects/update_testing_values", - json={ - "workspaceId": workspace_id, - "builderProjectId": builder_project_id, - "testingValues": testing_values, - "spec": spec, - }, + request=ConnectorBuilderProjectTestingValuesUpdate( + workspaceId=workspace_id, + builderProjectId=builder_project_id, + testingValues=ConnectorBuilderProjectTestingValues.model_validate(testing_values), + spec=SourceDefinitionSpecification.model_validate(spec), + ), + response_model=ConnectorBuilderProjectTestingValues, api_root=api_root, config_api_root=config_api_root, client_id=client_id, @@ -2434,7 +2492,7 @@ def list_organizations_for_user_id( config_api_root: str | None = None, name_contains: str | None = None, limit: int | None = None, -) -> list[dict[str, Any]]: +) -> list[OrganizationRead]: """List organizations the given user is a member of. Uses the Config API endpoint: POST /v1/organizations/list_by_user_id @@ -2453,29 +2511,23 @@ def list_organizations_for_user_id( limit: Optional maximum number of organizations to return Returns: - List of organization dictionaries containing organizationId, organizationName, email, etc. + List of organization models containing organizationId, organizationName, email, etc. """ _validate_pagination_params(limit=limit) - result: list[dict[str, Any]] = [] + result: list[OrganizationRead] = [] page_size = PAGE_SIZE - payload: dict[str, Any] = { - "userId": user_id, - "pagination": { - "pageSize": page_size, - "rowOffset": 0, - }, - } - if name_contains is not None: - payload["nameContains"] = name_contains + row_offset = 0 while True: json_result = _make_config_api_request( path="/organizations/list_by_user_id", - json={ - **payload, - "pagination": payload["pagination"].copy(), - }, + request=ListOrganizationsByUserRequestBody( + userId=user_id, + pagination=Pagination(pageSize=page_size, rowOffset=row_offset), + nameContains=name_contains, + ), + response_model=OrganizationReadList, api_root=api_root, config_api_root=config_api_root, client_id=client_id, @@ -2483,17 +2535,7 @@ def list_organizations_for_user_id( bearer_token=bearer_token, ) - if not isinstance(json_result, dict): - raise AirbyteError( - message="The organizations API returned an unexpected response.", - context={"response": json_result}, - ) - organizations = json_result.get("organizations", []) - if not isinstance(organizations, list): - raise AirbyteError( - message="The organizations API returned an unexpected response.", - context={"response": json_result}, - ) + organizations = json_result.organizations if not organizations: break @@ -2506,7 +2548,7 @@ def list_organizations_for_user_id( if len(organizations) < page_size: break - payload["pagination"]["rowOffset"] += page_size + row_offset += page_size return result @@ -2521,7 +2563,7 @@ def list_workspaces_in_organization( config_api_root: str | None = None, name_contains: str | None = None, limit: int | None = None, -) -> list[dict[str, Any]]: +) -> list[WorkspaceRead]: """List workspaces within a specific organization. Uses the Config API endpoint: POST /v1/workspaces/list_by_organization_id @@ -2537,28 +2579,24 @@ def list_workspaces_in_organization( limit: Optional maximum number of workspaces to return Returns: - List of workspace dictionaries containing workspaceId, organizationId, name, slug, etc. + List of workspace models containing workspaceId, organizationId, name, slug, etc. """ _validate_pagination_params(limit=limit) - result: list[dict[str, Any]] = [] + result: list[WorkspaceRead] = [] page_size = 100 - # Build base payload - payload: dict[str, Any] = { - "organizationId": organization_id, - "pagination": { - "pageSize": page_size, - "rowOffset": 0, - }, - } - if name_contains: - payload["nameContains"] = name_contains + row_offset = 0 # Fetch pages until we have all results or reach the limit while True: json_result = _make_config_api_request( path="/workspaces/list_by_organization_id", - json=payload, + request=ListWorkspacesInOrganizationRequestBody( + organizationId=organization_id, + pagination=Pagination(pageSize=page_size, rowOffset=row_offset), + nameContains=name_contains, + ), + response_model=WorkspaceReadList, api_root=api_root, config_api_root=config_api_root, client_id=client_id, @@ -2566,7 +2604,7 @@ def list_workspaces_in_organization( bearer_token=bearer_token, ) - workspaces = json_result.get("workspaces", []) + workspaces = json_result.workspaces # If no results returned, we've exhausted all pages if not workspaces: @@ -2583,7 +2621,7 @@ def list_workspaces_in_organization( break # Bump offset for next iteration - payload["pagination"]["rowOffset"] += page_size + row_offset += page_size return result @@ -2596,7 +2634,7 @@ def get_workspace_organization_info( client_secret: SecretString | None, bearer_token: SecretString | None, config_api_root: str | None = None, -) -> dict[str, Any]: +) -> OrganizationInfoRead: """Get organization info for a workspace. Uses the Config API endpoint: POST /v1/workspaces/get_organization_info @@ -2613,27 +2651,22 @@ def get_workspace_organization_info( config_api_root: Optional explicit Config API root URL. Returns: - Dictionary containing organization info: + Generated organization info response: - organizationId: The organization ID - organizationName: The organization name - sso: Whether SSO is enabled - billing: Billing information (optional) """ - result = _make_config_api_request( + return _make_config_api_request( path="/workspaces/get_organization_info", - json={"workspaceId": workspace_id}, + request=WorkspaceIdRequestBody(workspaceId=workspace_id), + response_model=OrganizationInfoRead, api_root=api_root, config_api_root=config_api_root, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, ) - if isinstance(result, dict): - return result - raise AirbyteError( - message="The workspace API returned an unexpected response.", - context={"response": result}, - ) def get_connection_state( @@ -2660,15 +2693,17 @@ def get_connection_state( Returns: Dictionary containing the connection state. """ - return _make_config_api_request( + response = _make_config_api_request( path="/state/get", - json={"connectionId": connection_id}, + request=ConnectionIdRequestBody(connectionId=connection_id), + response_model=ConnectionState, api_root=api_root, config_api_root=config_api_root, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, ) + return response.model_dump(mode="json", by_alias=True, exclude_none=True) def replace_connection_state( @@ -2713,21 +2748,25 @@ def replace_connection_state( Dictionary containing the updated ConnectionState object. """ try: - return _make_config_api_request( + response = _make_config_api_request( path="/state/create_or_update_safe", - json={ - "connectionId": connection_id, - "connectionState": { - **connection_state_dict, - "connectionId": connection_id, - }, - }, + request=ConnectionStateCreateOrUpdate( + connectionId=connection_id, + connectionState=ConnectionState.model_validate( + { + **connection_state_dict, + "connectionId": connection_id, + } + ), + ), + response_model=ConnectionState, api_root=api_root, config_api_root=config_api_root, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, ) + return response.model_dump(mode="json", by_alias=True, exclude_none=True) except AirbyteError as ex: if ex.context and ex.context.get("status_code") == HTTPStatus.LOCKED: raise AirbyteConnectionSyncActiveError( @@ -2765,15 +2804,20 @@ def get_connection_catalog( Returns: Dictionary containing the connection info with syncCatalog. """ - return _make_config_api_request( + response = _make_config_api_request( path="/web_backend/connections/get", - json={"connectionId": connection_id, "withRefreshedCatalog": False}, + request=WebBackendConnectionRequestBody( + connectionId=connection_id, + withRefreshedCatalog=False, + ), + response_model=WebBackendConnectionRead, api_root=api_root, config_api_root=config_api_root, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, ) + return response.model_dump(mode="json", by_alias=True, exclude_none=True) def replace_connection_catalog( @@ -2805,21 +2849,23 @@ def replace_connection_catalog( Returns: Dictionary containing the updated WebBackendConnectionRead response. """ - return _make_config_api_request( + response = _make_config_api_request( path="/web_backend/connections/update", - json={ - "connectionId": connection_id, - "syncCatalog": configured_catalog_dict, + request=WebBackendConnectionUpdate( + connectionId=connection_id, + syncCatalog=AirbyteCatalog.model_validate(configured_catalog_dict), # Resets are destructive and cause customer-side data outage. # If a reset is desired, caller will need to decide & manage. - "skipReset": True, - }, + skipReset=True, + ), + response_model=WebBackendConnectionRead, api_root=api_root, config_api_root=config_api_root, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, ) + return response.model_dump(mode="json", by_alias=True, exclude_none=True) def get_organization_info( @@ -2830,7 +2876,7 @@ def get_organization_info( client_id: SecretString | None, client_secret: SecretString | None, bearer_token: SecretString | None, -) -> dict[str, Any]: +) -> OrganizationInfoRead: """Get organization info including billing status. Uses the Config API endpoint: POST /v1/organizations/get_organization_info @@ -2844,27 +2890,22 @@ def get_organization_info( config_api_root: Optional explicit Config API root URL. Returns: - Dictionary containing organization info: + Generated organization info response: - organizationId: The organization ID - organizationName: The organization name - sso: Whether SSO is enabled - billing: Billing information (optional, contains paymentStatus, subscriptionStatus, etc.) """ - result = _make_config_api_request( + return _make_config_api_request( path="/organizations/get_organization_info", - json={"organizationId": organization_id}, + request=OrganizationIdRequestBody(organizationId=organization_id), + response_model=OrganizationInfoRead, api_root=api_root, config_api_root=config_api_root, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, ) - if isinstance(result, dict): - return result - raise AirbyteError( - message="The organization API returned an unexpected response.", - context={"response": result}, - ) def get_user_id_from_bearer_token(bearer_token: SecretString) -> str: @@ -2905,27 +2946,18 @@ def get_user_by_auth_id( client_id: SecretString | None, client_secret: SecretString | None, bearer_token: SecretString | None, -) -> dict[str, Any]: +) -> UserRead: """Get an Airbyte user by the authentication provider user ID.""" - result = _make_config_api_request( + return _make_config_api_request( path="/users/get_by_auth_id", - json={ - "authUserId": auth_user_id, - "authProvider": "keycloak", - }, + request=UserAuthIdRequestBody(authUserId=auth_user_id), + response_model=UserRead, api_root=api_root, config_api_root=config_api_root, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, ) - if isinstance(result, dict): - return result - - raise AirbyteError( - message="The user API returned an unexpected response.", - context={"response": result}, - ) def list_permissions_for_user( @@ -2936,28 +2968,19 @@ def list_permissions_for_user( client_id: SecretString | None, client_secret: SecretString | None, bearer_token: SecretString | None, -) -> list[dict[str, Any]]: +) -> list[PermissionRead]: """List permissions granted to an Airbyte user.""" result = _make_config_api_request( path="/permissions/list_by_user", - json={"userId": user_id}, + request=UserIdRequestBody(userId=user_id), + response_model=PermissionReadList, api_root=api_root, config_api_root=config_api_root, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, ) - if isinstance(result, list): - return result - - permissions = result.get("permissions") if isinstance(result, dict) else None - if isinstance(permissions, list): - return permissions - - raise AirbyteError( - message="The permissions API returned an unexpected response.", - context={"response": result}, - ) + return result.permissions # Billing status constants (using tuples for safe `in` checks with unhashable types) diff --git a/airbyte/cloud/client.py b/airbyte/cloud/client.py index 1963e15fa..4e81c5f9d 100644 --- a/airbyte/cloud/client.py +++ b/airbyte/cloud/client.py @@ -84,7 +84,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, NoReturn, overload +from typing import TYPE_CHECKING, NoReturn, overload from airbyte import exceptions as exc from airbyte._util import api_util @@ -96,7 +96,9 @@ if TYPE_CHECKING: - from collections.abc import Callable, Mapping + from collections.abc import Callable + + from airbyte_server_models._config_api import OrganizationRead from airbyte.secrets.base import SecretString @@ -408,7 +410,24 @@ def matches_name(workspace_name: str) -> bool: name_contains=name_contains or name, limit=None if name is not None or name_filter is not None else limit, ) - workspace_infos = [CloudWorkspaceInfo.from_mapping(workspace) for workspace in workspaces] + workspace_infos = [ + CloudWorkspaceInfo.model_validate( + { + "workspaceId": str(workspace.workspaceId), + "name": workspace.name, + "organizationId": str(workspace.organizationId), + "notifications": ( + [ + notification.model_dump(mode="json", by_alias=True) + for notification in workspace.notifications + ] + if workspace.notifications is not None + else {} + ), + } + ) + for workspace in workspaces + ] if name is not None: workspace_infos = [workspace for workspace in workspace_infos if workspace.name == name] if name_filter is not None: @@ -480,8 +499,8 @@ def _get_workspace_parent_organization_id(self, workspace_id: str) -> str: client_secret=self.client_secret, bearer_token=self._get_config_api_bearer_token(), ) - resolved_organization_id = organization.get("organizationId") - if isinstance(resolved_organization_id, str) and resolved_organization_id: + resolved_organization_id = str(organization.organizationId) + if resolved_organization_id: return resolved_organization_id raise exc.PyAirbyteInputError( message="The workspace response did not include an organization ID.", @@ -508,12 +527,7 @@ def _get_authenticated_user_id(self) -> str: client_secret=self.client_secret, bearer_token=bearer_token, ) - user_id = user.get("userId") - if not isinstance(user_id, str) or not user_id: - raise exc.PyAirbyteInputError( - message="The Airbyte user response did not include a user ID.", - context={"response": user}, - ) + user_id = str(user.userId) self._authenticated_user_id = user_id return self._authenticated_user_id @@ -532,12 +546,10 @@ def _get_membership_organization_ids(self) -> tuple[str, ...]: ) organization_ids: list[str] = [] for permission in permissions: - permission_organization_id = permission.get("organizationId") - if ( - isinstance(permission_organization_id, str) - and permission_organization_id - and permission_organization_id not in organization_ids - ): + permission_organization_id = ( + str(permission.organizationId) if permission.organizationId is not None else None + ) + if permission_organization_id and permission_organization_id not in organization_ids: organization_ids.append(permission_organization_id) self._membership_organization_ids = tuple(organization_ids) return self._membership_organization_ids @@ -562,9 +574,7 @@ def _get_organization_candidates( except AirbyteError: pass else: - candidate_name = organization_info.get("organizationName") - if isinstance(candidate_name, str): - organization_name = candidate_name + organization_name = organization_info.organizationName candidates.append( { "organization_id": organization_id, @@ -640,7 +650,7 @@ def _list_organizations_by_user_id( """List organizations via the Config API, with server-side search and paging.""" user_id = self._get_authenticated_user_id() return [ - self._organization_from_mapping(organization) + self._organization_from_model(organization) for organization in api_util.list_organizations_for_user_id( user_id=user_id, api_root=self.public_api_root, @@ -653,15 +663,15 @@ def _list_organizations_by_user_id( ) ] - def _organization_from_mapping( + def _organization_from_model( self, - organization: Mapping[str, Any], + organization: OrganizationRead, ) -> CloudOrganization: - """Build a `CloudOrganization` from a Config API organization mapping.""" + """Build a `CloudOrganization` from a Config API organization model.""" return CloudOrganization( - organization_id=organization["organizationId"], - organization_name=organization.get("organizationName"), - email=organization.get("email"), + organization_id=str(organization.organizationId), + organization_name=organization.organizationName, + email=organization.email, client_id=self.client_id, client_secret=self.client_secret, bearer_token=self.bearer_token, @@ -707,9 +717,15 @@ def _get_organization_by_id(self, organization_id: str) -> CloudOrganization | N ) except AirbyteError: return None - if not isinstance(organization_info.get("organizationId"), str): - return None - return self._organization_from_mapping(organization_info) + return CloudOrganization( + organization_id=str(organization_info.organizationId), + organization_name=organization_info.organizationName, + client_id=self.client_id, + client_secret=self.client_secret, + bearer_token=self.bearer_token, + public_api_root=self.public_api_root, + config_api_root=self.config_api_root, + ) def _search_organizations_by_name( self, diff --git a/airbyte/cloud/connectors.py b/airbyte/cloud/connectors.py index 71387880e..78d112bd7 100644 --- a/airbyte/cloud/connectors.py +++ b/airbyte/cloud/connectors.py @@ -501,11 +501,13 @@ def connector_builder_project_id(self) -> str | None: bearer_token=self.workspace.bearer_token, config_api_root=self.workspace.config_api_root, ) - self._connector_builder_project_id = result.get("builderProjectId") + self._connector_builder_project_id = str(result.builderProjectId) self._connector_builder_project_id_fetched = True # The builder project may live in a different workspace than the caller's. # We must use the project's owning workspace ID when fetching its data. - self._builder_project_workspace_id = result.get("workspaceId") + self._builder_project_workspace_id = ( + str(result.workspaceId) if result.workspaceId is not None else None + ) return self._connector_builder_project_id @@ -573,7 +575,7 @@ def get_builder_project_data( client_secret=self.workspace.client_secret, bearer_token=self.workspace.bearer_token, config_api_root=self.workspace.config_api_root, - ) + ).model_dump(mode="json", by_alias=True, exclude_none=True) return self._builder_project_data @property diff --git a/airbyte/cloud/organizations.py b/airbyte/cloud/organizations.py index 1ab5e6a86..9aec6f688 100644 --- a/airbyte/cloud/organizations.py +++ b/airbyte/cloud/organizations.py @@ -4,13 +4,17 @@ from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING from airbyte._util import api_util from airbyte.cloud._credentials import _AirbyteCredentials from airbyte.secrets.base import SecretString +if TYPE_CHECKING: + from airbyte_server_models._config_api import OrganizationInfoRead + + logger = logging.getLogger(__name__) @@ -51,16 +55,20 @@ def __init__( config_api_root=config_api_root, organization_id=organization_id, ) - self._organization_info: dict[str, Any] | None = None + self._organization_info: OrganizationInfoRead | None = None self._organization_info_fetch_failed: bool = False - def _fetch_organization_info(self, *, force_refresh: bool = False) -> dict[str, Any]: + def _fetch_organization_info( + self, + *, + force_refresh: bool = False, + ) -> OrganizationInfoRead | None: """Fetch and cache organization info including billing status.""" if force_refresh: self._organization_info_fetch_failed = False if self._organization_info_fetch_failed and self._organization_info is None: - return {} + return None if not force_refresh and self._organization_info is not None: return self._organization_info @@ -78,7 +86,7 @@ def _fetch_organization_info(self, *, force_refresh: bool = False) -> dict[str, logger.debug("Failed to fetch organization info.", exc_info=ex) if self._organization_info is None: self._organization_info_fetch_failed = True - return self._organization_info or {} + return self._organization_info else: return self._organization_info @@ -88,27 +96,32 @@ def organization_name(self) -> str | None: if self._organization_name is not None: return self._organization_name info = self._fetch_organization_info() - return info.get("organizationName") + return getattr(info, "organizationName", None) @property def email(self) -> str | None: """Email associated with the organization.""" if self._email is not None: return self._email - info = self._fetch_organization_info() - return info.get("email") + return self._email @property def payment_status(self) -> str | None: """Payment status of the organization.""" info = self._fetch_organization_info() - return (info.get("billing") or {}).get("paymentStatus") + billing = getattr(info, "billing", None) + return billing.paymentStatus.value if billing is not None else None @property def subscription_status(self) -> str | None: """Subscription status of the organization.""" info = self._fetch_organization_info() - return (info.get("billing") or {}).get("subscriptionStatus") + billing = getattr(info, "billing", None) + return ( + billing.subscriptionStatus.value + if billing is not None and billing.subscriptionStatus is not None + else None + ) @property def is_account_locked(self) -> bool: diff --git a/airbyte/cloud/sync_results.py b/airbyte/cloud/sync_results.py index e2a88729d..9865439a5 100644 --- a/airbyte/cloud/sync_results.py +++ b/airbyte/cloud/sync_results.py @@ -103,9 +103,9 @@ import time from collections.abc import Iterator, Mapping from dataclasses import asdict, dataclass -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, final -from typing_extensions import final +from airbyte_server_models._config_api import JobIdRequestBody, JobInfoRead # noqa: PLC2701 from airbyte_cdk.utils.datetime_helpers import ab_datetime_parse @@ -318,12 +318,13 @@ def start_time(self) -> datetime: api_root=self.workspace.api_root, config_api_root=self.workspace.config_api_root, path="/jobs/get", - json={"id": self.job_id}, + request=JobIdRequestBody(id=self.job_id), + response_model=JobInfoRead, client_id=self.workspace.client_id, client_secret=self.workspace.client_secret, bearer_token=self.workspace.bearer_token, ) - raw_start_time = job_info_raw.get("startTime") + raw_start_time = job_info_raw.job.startedAt if job_info_raw.job else None if raw_start_time: return ab_datetime_parse(raw_start_time) raise @@ -337,13 +338,12 @@ def _fetch_job_with_attempts(self) -> dict[str, Any]: api_root=self.workspace.api_root, config_api_root=self.workspace.config_api_root, path="/jobs/get", - json={ - "id": self.job_id, - }, + request=JobIdRequestBody(id=self.job_id), + response_model=JobInfoRead, client_id=self.workspace.client_id, client_secret=self.workspace.client_secret, bearer_token=self.workspace.bearer_token, - ) + ).model_dump(mode="json", by_alias=True, exclude_none=True) return self._job_with_attempts_info def get_attempts(self) -> list[SyncAttempt]: diff --git a/airbyte/cloud/workspaces.py b/airbyte/cloud/workspaces.py index 58f5539e7..135979099 100644 --- a/airbyte/cloud/workspaces.py +++ b/airbyte/cloud/workspaces.py @@ -62,6 +62,8 @@ if TYPE_CHECKING: from collections.abc import Callable + from airbyte_server_models._config_api import OrganizationInfoRead + from airbyte.secrets.base import SecretString from airbyte.sources.base import Source @@ -214,7 +216,7 @@ def workspace_url(self) -> str | None: return f"{get_web_url_root(self.api_root)}/workspaces/{self.workspace_id}" @cached_property - def _organization_info(self) -> dict[str, Any]: + def _organization_info(self) -> OrganizationInfoRead: """Fetch and cache organization info for this workspace. Uses the Config API endpoint for an efficient O(1) lookup. @@ -275,8 +277,8 @@ def get_organization( raise return None - organization_id = info.get("organizationId") - organization_name = info.get("organizationName") + organization_id = str(info.organizationId) + organization_name = info.organizationName # Validate that both organization_id and organization_name are non-null and non-empty if not organization_id or not organization_name: diff --git a/tests/unit_tests/test_cloud_api_util.py b/tests/unit_tests/test_cloud_api_util.py index 03ff6dc58..7a6f55469 100644 --- a/tests/unit_tests/test_cloud_api_util.py +++ b/tests/unit_tests/test_cloud_api_util.py @@ -3,8 +3,8 @@ from __future__ import annotations -from collections.abc import Callable from types import SimpleNamespace +from uuid import UUID import pytest import requests @@ -17,6 +17,28 @@ ) from airbyte.secrets.base import SecretString from airbyte_api import api, models +from airbyte_server_models._config_api import ( + OrganizationInfoRead, + OrganizationRead, + OrganizationReadList, + PermissionRead, + PermissionReadList, + PermissionType, + CheckConnectionRead, + JobConfigType, + Status4, + SynchronousJobRead, + UserRead, +) + + +USER_ID = UUID("00000000-0000-0000-0000-000000000001") +ORGANIZATION_ID = UUID("00000000-0000-0000-0000-000000000002") +PERMISSION_ID = UUID("00000000-0000-0000-0000-000000000003") + + +def _id_for_index(index: int) -> UUID: + return UUID(int=index + 10) def _job_response(job_id: int) -> models.JobResponse: @@ -150,71 +172,19 @@ def test_get_user_id_from_bearer_token_rejects_invalid_tokens( api_util.get_user_id_from_bearer_token(SecretString(token)) -@pytest.mark.parametrize( - ( - "helper", - "kwargs", - "expected_path", - "expected_json", - "fake_response", - "expected_result", - ), - [ - pytest.param( - api_util.get_user_by_auth_id, - {"auth_user_id": "auth-user-id"}, - "/users/get_by_auth_id", - {"authUserId": "auth-user-id", "authProvider": "keycloak"}, - {"userId": "user-id"}, - {"userId": "user-id"}, - id="user-by-auth-id", - ), - pytest.param( - api_util.list_permissions_for_user, - {"user_id": "user-id"}, - "/permissions/list_by_user", - {"userId": "user-id"}, - [{"permissionType": "organization_member", "organizationId": "org-id"}], - [{"permissionType": "organization_member", "organizationId": "org-id"}], - id="permissions-list", - ), - pytest.param( - api_util.list_permissions_for_user, - {"user_id": "user-id"}, - "/permissions/list_by_user", - {"userId": "user-id"}, - { - "permissions": [ - { - "permissionType": "organization_member", - "organizationId": "org-id", - } - ] - }, - [{"permissionType": "organization_member", "organizationId": "org-id"}], - id="permissions-envelope", - ), - ], -) -def test_config_api_helpers_forward_requests( +def test_get_user_by_auth_id_forwards_typed_request( monkeypatch: pytest.MonkeyPatch, - helper: Callable[..., object], - kwargs: dict[str, str], - expected_path: str, - expected_json: dict[str, str], - fake_response: object, - expected_result: object, ) -> None: captured: dict[str, object] = {} def fake_config_request(**request_kwargs: object) -> object: captured.update(request_kwargs) - return fake_response + return UserRead(userId=USER_ID, email="user@example.com", metadata={}) monkeypatch.setattr(api_util, "_make_config_api_request", fake_config_request) - result = helper( - **kwargs, + result = api_util.get_user_by_auth_id( + "auth-user-id", api_root="https://api.example", config_api_root="https://config.example", client_id=None, @@ -222,110 +192,105 @@ def fake_config_request(**request_kwargs: object) -> object: bearer_token=SecretString("token"), ) - assert result == expected_result - assert captured["path"] == expected_path - assert captured["json"] == expected_json + assert result.userId == USER_ID + assert captured["path"] == "/users/get_by_auth_id" + assert captured["request"].model_dump(mode="json", exclude_none=True) == { + "authUserId": "auth-user-id" + } assert captured["config_api_root"] == "https://config.example" -@pytest.mark.parametrize( - ("helper", "kwargs", "response", "expected_message"), - [ - pytest.param( - api_util.get_user_by_auth_id, - {"auth_user_id": "auth-user-id"}, - [], - "user API returned an unexpected response", - id="user-list", - ), - pytest.param( - api_util.get_user_by_auth_id, - {"auth_user_id": "auth-user-id"}, - "unexpected", - "user API returned an unexpected response", - id="user-string", - ), - pytest.param( - api_util.list_permissions_for_user, - {"user_id": "user-id"}, - "unexpected", - "permissions API returned an unexpected response", - id="permissions-string", - ), - pytest.param( - api_util.list_permissions_for_user, - {"user_id": "user-id"}, - {"permissions": {}}, - "permissions API returned an unexpected response", - id="permissions-non-list-envelope", - ), - pytest.param( - api_util.list_organizations_for_user_id, - {"user_id": "user-id"}, - [], - "organizations API returned an unexpected response", - id="organizations-list", - ), - pytest.param( - api_util.list_organizations_for_user_id, - {"user_id": "user-id"}, - {"organizations": {}}, - "organizations API returned an unexpected response", - id="organizations-non-list-envelope", - ), - pytest.param( - api_util.get_organization_info, - {"organization_id": "organization-id"}, - [], - "organization API returned an unexpected response", - id="organization-list", - ), - pytest.param( - api_util.get_organization_info, - {"organization_id": "organization-id"}, - "unexpected", - "organization API returned an unexpected response", - id="organization-string", - ), - pytest.param( - api_util.get_workspace_organization_info, - {"workspace_id": "workspace-id"}, - [], - "workspace API returned an unexpected response", - id="workspace-list", - ), - pytest.param( - api_util.get_workspace_organization_info, - {"workspace_id": "workspace-id"}, - "unexpected", - "workspace API returned an unexpected response", - id="workspace-string", - ), - ], -) -def test_config_api_helpers_reject_unexpected_response( +def test_list_permissions_for_user_forwards_typed_request( monkeypatch: pytest.MonkeyPatch, - helper: Callable[..., object], - kwargs: dict[str, str], - response: object, - expected_message: str, ) -> None: - monkeypatch.setattr( - api_util, - "_make_config_api_request", - lambda **_: response, + captured: dict[str, object] = {} + + permission = PermissionRead( + permissionId=PERMISSION_ID, + permissionType=PermissionType.organization_member, + userId=USER_ID, + organizationId=ORGANIZATION_ID, ) - with pytest.raises(AirbyteError, match=expected_message) as exc_info: - helper( - **kwargs, + def fake_config_request(**request_kwargs: object) -> object: + captured.update(request_kwargs) + return PermissionReadList(permissions=[permission]) + + monkeypatch.setattr(api_util, "_make_config_api_request", fake_config_request) + + result = api_util.list_permissions_for_user( + str(USER_ID), + api_root="https://api.example", + client_id=None, + client_secret=None, + bearer_token=SecretString("token"), + ) + + assert result == [permission] + assert captured["request"].model_dump(mode="json", exclude_none=True) == { + "userId": str(USER_ID) + } + + +def test_make_config_api_request_wraps_response_validation_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + response = requests.Response() + response.status_code = 200 + response._content = b'{"invalid": true}' + response.url = "https://config.example/users/get_by_auth_id" + monkeypatch.setattr(requests, "request", lambda **_: response) + + with pytest.raises( + AirbyteError, match="did not match the expected schema" + ) as exc_info: + api_util._make_config_api_request( api_root="https://api.example", + path="/users/get_by_auth_id", + request=api_util.UserAuthIdRequestBody(authUserId="auth-user-id"), + response_model=UserRead, client_id=None, client_secret=None, bearer_token=SecretString("token"), + config_api_root="https://config.example", ) - assert exc_info.value.context == {"response": response} + assert exc_info.value.context == { + "full_url": "https://config.example/users/get_by_auth_id", + "path": "/users/get_by_auth_id", + "response": '{"invalid": true}', + } + + +def test_get_organization_info_forwards_typed_request( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, object] = {} + + monkeypatch.setattr( + api_util, + "_make_config_api_request", + lambda **kwargs: captured.update(kwargs) + or OrganizationInfoRead( + organizationId=ORGANIZATION_ID, + organizationName="Organization", + sso=False, + scim=False, + ), + ) + + result = api_util.get_organization_info( + str(ORGANIZATION_ID), + api_root="https://api.example", + client_id=None, + client_secret=None, + bearer_token=SecretString("token"), + ) + + assert result.organizationId == ORGANIZATION_ID + assert captured["request"].model_dump(mode="json", exclude_none=True) == { + "organizationId": str(ORGANIZATION_ID) + } @pytest.mark.parametrize( @@ -359,29 +324,38 @@ def test_list_organizations_for_user_id_paginates_and_forwards_filters( ) -> None: requests: list[dict[str, object]] = [] pages = [ - { - "organizations": [ - {"organizationId": f"organization-{index}"} for index in range(100) + OrganizationReadList( + organizations=[ + OrganizationRead( + organizationId=_id_for_index(index), + organizationName=f"Organization {index}", + email=f"organization-{index}@example.com", + ) + for index in range(100) ] - }, - { - "organizations": [ - {"organizationId": f"organization-{index}"} for index in range(100, 200) + ), + OrganizationReadList( + organizations=[ + OrganizationRead( + organizationId=_id_for_index(index), + organizationName=f"Organization {index}", + email=f"organization-{index}@example.com", + ) + for index in range(100, 200) ] - }, - {"organizations": []}, + ), + OrganizationReadList(organizations=[]), ] - def fake_config_request(**kwargs: object) -> dict[str, object]: - request = kwargs["json"] - assert isinstance(request, dict) - requests.append(request) + def fake_config_request(**kwargs: object) -> OrganizationReadList: + request = kwargs["request"] + requests.append(request.model_dump(mode="json", exclude_none=True)) return pages.pop(0) monkeypatch.setattr(api_util, "_make_config_api_request", fake_config_request) result = api_util.list_organizations_for_user_id( - "user-id", + str(USER_ID), api_root="https://api.airbyte.com/v1/", client_id=SecretString("client-id"), client_secret=SecretString("client-secret"), @@ -390,12 +364,14 @@ def fake_config_request(**kwargs: object) -> dict[str, object]: limit=limit, ) - result_ids = [organization["organizationId"] for organization in result] + result_ids = [str(organization.organizationId) for organization in result] assert len(result_ids) == expected_count - assert result_ids[0] == "organization-0" - assert result_ids[-1] == expected_last + assert result_ids[0] == str(_id_for_index(0)) + assert result_ids[-1] == str( + _id_for_index(int(expected_last.removeprefix("organization-"))) + ) assert requests[0] == { - "userId": "user-id", + "userId": str(USER_ID), "pagination": {"pageSize": 100, "rowOffset": 0}, **({"nameContains": name_contains} if name_contains is not None else {}), } @@ -403,6 +379,50 @@ def fake_config_request(**kwargs: object) -> dict[str, object]: assert len(requests) == request_count +@pytest.mark.parametrize( + ("connector_type", "request_field"), + [("source", "sourceId"), ("destination", "destinationId")], +) +def test_check_connector_uses_connector_specific_request_body( + monkeypatch: pytest.MonkeyPatch, + connector_type: str, + request_field: str, +) -> None: + captured: dict[str, object] = {} + actor_id = str(_id_for_index(50)) + + def fake_config_request(**kwargs: object) -> CheckConnectionRead: + captured.update(kwargs) + return CheckConnectionRead( + status=Status4.succeeded, + jobInfo=SynchronousJobRead( + id=_id_for_index(51), + configType=( + JobConfigType.check_connection_source + if connector_type == "source" + else JobConfigType.check_connection_destination + ), + createdAt=1, + endedAt=2, + succeeded=True, + ), + ) + + monkeypatch.setattr(api_util, "_make_config_api_request", fake_config_request) + + assert api_util.check_connector( + actor_id=actor_id, + connector_type=connector_type, # type: ignore[arg-type] + api_root="https://api.example", + client_id=None, + client_secret=None, + bearer_token=SecretString("token"), + ) == (True, None) + assert captured["request"].model_dump(mode="json", exclude_none=True) == { + request_field: actor_id + } + + def test_create_workspace_forwards_request( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/tests/unit_tests/test_cloud_credentials.py b/tests/unit_tests/test_cloud_credentials.py index a1183b6c2..7234a3e86 100644 --- a/tests/unit_tests/test_cloud_credentials.py +++ b/tests/unit_tests/test_cloud_credentials.py @@ -22,6 +22,16 @@ ) from airbyte.mcp import cloud as mcp_cloud from airbyte.secrets.base import SecretString +from airbyte_server_models._config_api import ( + Notification, + NotificationType, + OrganizationInfoRead, + OrganizationRead, + PermissionRead, + PermissionType, + UserRead, + WorkspaceRead, +) def _raise(error: Exception) -> Callable[..., NoReturn]: @@ -31,6 +41,70 @@ def _raiser(*_args: object, **_kwargs: object) -> NoReturn: return _raiser +def _user_response(user_id: str = "user-id") -> UserRead: + return UserRead.model_construct( + userId=user_id, + email="user@example.com", + metadata={}, + ) + + +def _organization_info_response( + *, + organization_id: str = "organization-id", + organization_name: str = "Organization", +) -> OrganizationInfoRead: + return OrganizationInfoRead.model_construct( + organizationId=organization_id, + organizationName=organization_name, + sso=False, + scim=False, + ) + + +def _permission_response( + *, + organization_id: str | None = "organization-id", +) -> PermissionRead: + return PermissionRead.model_construct( + permissionId="permission-id", + permissionType=PermissionType.organization_member, + userId="user-id", + organizationId=organization_id, + ) + + +def _organization_response( + *, + organization_id: str = "organization-id", + organization_name: str = "Organization", + email: str = "test@example.com", +) -> OrganizationRead: + return OrganizationRead.model_construct( + organizationId=organization_id, + organizationName=organization_name, + email=email, + ) + + +def _workspace_config_response( + *, + workspace_id: str, + name: str, + organization_id: str = "organization-id", + notifications: list[Notification] | None = None, +) -> WorkspaceRead: + return WorkspaceRead.model_construct( + workspaceId=workspace_id, + customerId="customer-id", + name=name, + slug=name.casefold().replace(" ", "-"), + initialSetupComplete=False, + organizationId=organization_id, + notifications=notifications, + ) + + def _patch_workspace_discovery( monkeypatch: pytest.MonkeyPatch, *, @@ -46,20 +120,27 @@ def _patch_workspace_discovery( lambda _: "auth-user-id", ) monkeypatch.setattr( - api_util, - "get_user_by_auth_id", - lambda *_, **__: {"userId": "user-id"}, + api_util, "get_user_by_auth_id", lambda *_, **__: _user_response() ) monkeypatch.setattr( api_util, "list_permissions_for_user", - lambda *_, **__: permissions or [], + lambda *_, **__: [ + _permission_response( + organization_id=( + str(permission["organizationId"]) + if permission.get("organizationId") is not None + else None + ) + ) + for permission in (permissions or []) + ], ) - def fake_get_workspace_organization_info(**_: object) -> dict[str, object]: + def fake_get_workspace_organization_info(**_: object) -> OrganizationInfoRead: if parent_organization_id is None: pytest.fail("workspace parent lookup should not be called") - return {"organizationId": parent_organization_id} + return _organization_info_response(organization_id=parent_organization_id) monkeypatch.setattr( api_util, @@ -74,9 +155,15 @@ def fake_get_workspace_organization_info(**_: object) -> dict[str, object]: def fake_list_workspaces_in_organization( **kwargs: object, - ) -> list[dict[str, object]]: + ) -> list[WorkspaceRead]: captured.update(kwargs) - return org_scoped_result if org_scoped_result is not None else [] + return [ + _workspace_config_response( + workspace_id=str(workspace["workspaceId"]), + name=str(workspace["name"]), + ) + for workspace in (org_scoped_result or []) + ] monkeypatch.setattr( api_util, @@ -441,13 +528,17 @@ def fake_list_workspaces_in_organization( *, limit: int | None = None, **_: object, - ) -> list[dict[str, object]]: + ) -> list[WorkspaceRead]: nonlocal captured_limit captured_limit = limit return [ - {"name": "miss", "workspaceId": "workspace-miss"}, - {"name": "target-one", "workspaceId": "workspace-target-one"}, - {"name": "target-two", "workspaceId": "workspace-target-two"}, + _workspace_config_response(workspace_id="workspace-miss", name="miss"), + _workspace_config_response( + workspace_id="workspace-target-one", name="target-one" + ), + _workspace_config_response( + workspace_id="workspace-target-two", name="target-two" + ), ] monkeypatch.setattr( @@ -477,11 +568,13 @@ def test_cloud_client_list_workspaces_matches_exact_name_after_server_filter( def fake_list_workspaces_in_organization( **kwargs: object, - ) -> list[dict[str, object]]: + ) -> list[WorkspaceRead]: captured.update(kwargs) return [ - {"name": "Production-old", "workspaceId": "workspace-production-old"}, - {"name": "Prod", "workspaceId": "workspace-prod"}, + _workspace_config_response( + workspace_id="workspace-production-old", name="Production-old" + ), + _workspace_config_response(workspace_id="workspace-prod", name="Prod"), ] monkeypatch.setattr( @@ -539,13 +632,19 @@ def test_cloud_client_list_workspaces_accepts_api_notification_list( ) -> None: def fake_list_workspaces_in_organization( **_: object, - ) -> list[dict[str, object]]: + ) -> list[WorkspaceRead]: return [ - { - "workspaceId": "workspace-id", - "name": "Workspace", - "notifications": [{"sendOnSuccess": True}], - } + _workspace_config_response( + workspace_id="workspace-id", + name="Workspace", + notifications=[ + Notification.model_construct( + notificationType=NotificationType.slack, + sendOnSuccess=True, + sendOnFailure=False, + ) + ], + ) ] monkeypatch.setattr( @@ -560,7 +659,7 @@ def fake_list_workspaces_in_organization( ).list_workspaces() assert len(workspaces) == 1 - assert workspaces[0].notifications == [{"sendOnSuccess": True}] + assert workspaces[0].notifications[0]["sendOnSuccess"] is True def test_cloud_workspace_info_accepts_api_notification_mapping() -> None: @@ -764,11 +863,10 @@ def test_cloud_client_get_organization_uses_default_organization_id( monkeypatch.setattr( api_util, "get_organization_info", - lambda **_: { - "organizationId": "default-org", - "organizationName": "Default Org", - "email": "test@example.com", - }, + lambda **_: _organization_info_response( + organization_id="default-org", + organization_name="Default Org", + ), ) organization = CloudClient( @@ -816,27 +914,35 @@ def test_cloud_client_get_organization_resolves_default_context( api_util, "get_user_id_from_bearer_token", lambda _: "auth-user-id" ) monkeypatch.setattr( - api_util, "get_user_by_auth_id", lambda *_, **__: {"userId": "user-id"} + api_util, "get_user_by_auth_id", lambda *_, **__: _user_response() ) monkeypatch.setattr( api_util, "list_permissions_for_user", - lambda *_, **__: permissions or [], + lambda *_, **__: [ + _permission_response( + organization_id=( + str(permission["organizationId"]) + if permission.get("organizationId") is not None + else None + ) + ) + for permission in (permissions or []) + ], ) monkeypatch.setattr( api_util, "get_workspace_organization_info", - lambda **_: {"organizationId": parent_organization_id} + lambda **_: _organization_info_response(organization_id=parent_organization_id) if parent_organization_id is not None else pytest.fail("workspace parent lookup should not be called"), ) monkeypatch.setattr( api_util, "get_organization_info", - lambda **kwargs: { - "organizationId": kwargs["organization_id"], - "organizationName": "Organization", - }, + lambda **kwargs: _organization_info_response( + organization_id=str(kwargs["organization_id"]), + ), ) organization = CloudClient(bearer_token="token", **client_kwargs).get_organization() @@ -856,9 +962,9 @@ def test_cloud_client_get_organization_rejects_ambiguous_default_context( monkeypatch.setattr( api_util, "get_organization_info", - lambda **kwargs: { - "organizationName": f"Organization {kwargs['organization_id']}" - }, + lambda **kwargs: _organization_info_response( + organization_name=f"Organization {kwargs['organization_id']}", + ), ) with pytest.raises(PyAirbyteInputError) as exc_info: @@ -925,13 +1031,9 @@ def test_cloud_client_get_organization_uses_single_config_lookup( ) -> None: calls: list[dict[str, object]] = [] - def fake_get_organization_info(**kwargs: object) -> dict[str, object]: + def fake_get_organization_info(**kwargs: object) -> OrganizationInfoRead: calls.append(kwargs) - return { - "organizationId": "organization-id", - "organizationName": "Organization", - "email": "test@example.com", - } + return _organization_info_response() monkeypatch.setattr(api_util, "get_organization_info", fake_get_organization_info) monkeypatch.setattr( @@ -1030,7 +1132,7 @@ def test_cloud_client_list_organizations_uses_config_api_for_filter_or_limit( def fake_list_organizations_for_user_id( **kwargs: object, - ) -> list[dict[str, object]]: + ) -> list[OrganizationRead]: captured.update(kwargs) filtered = organizations if kwargs["name_contains"] is not None: @@ -1042,11 +1144,11 @@ def fake_list_organizations_for_user_id( ] limit = kwargs["limit"] return [ - { - "organizationId": organization.organization_id, - "organizationName": organization.organization_name, - "email": organization.email, - } + _organization_response( + organization_id=organization.organization_id, + organization_name=organization.organization_name, + email=organization.email, + ) for organization in filtered[: limit if isinstance(limit, int) else None] ] @@ -1059,7 +1161,7 @@ def fake_list_organizations_for_user_id( api_util, "get_user_id_from_bearer_token", lambda _: "auth-user-id" ) monkeypatch.setattr( - api_util, "get_user_by_auth_id", lambda *_, **__: {"userId": "user-id"} + api_util, "get_user_by_auth_id", lambda *_, **__: _user_response() ) result = CloudClient(bearer_token="token").list_organizations( @@ -1090,7 +1192,7 @@ def test_cloud_client_list_organizations_falls_back_to_public_listing( monkeypatch.setattr( api_util, "get_user_by_auth_id", - lambda *_, **__: {"userId": "user-id"}, + lambda *_, **__: _user_response(), ) monkeypatch.setattr( api_util, @@ -1127,10 +1229,10 @@ def fake_get_bearer_token(**_: object) -> SecretString: def fake_list_organizations_for_user_id( **kwargs: object, - ) -> list[dict[str, object]]: + ) -> list[OrganizationRead]: nonlocal captured_bearer_token captured_bearer_token = kwargs["bearer_token"] - return [{"organizationId": "organization-id"}] + return [_organization_response(organization_id="organization-id")] monkeypatch.setattr(api_util, "get_bearer_token", fake_get_bearer_token) monkeypatch.setattr( @@ -1141,7 +1243,7 @@ def fake_list_organizations_for_user_id( monkeypatch.setattr( api_util, "get_user_by_auth_id", - lambda *_, **__: {"userId": "user-id"}, + lambda *_, **__: _user_response(), ) monkeypatch.setattr( api_util, @@ -1173,12 +1275,9 @@ def fake_get_bearer_token(**_: object) -> SecretString: token_calls += 1 return issued_token - def fake_get_organization_info(**kwargs: object) -> dict[str, object]: + def fake_get_organization_info(**kwargs: object) -> OrganizationInfoRead: captured_bearer_tokens.append(kwargs["bearer_token"]) - return { - "organizationId": "organization-id", - "organizationName": "Organization", - } + return _organization_info_response() monkeypatch.setattr(api_util, "get_bearer_token", fake_get_bearer_token) monkeypatch.setattr(api_util, "get_organization_info", fake_get_organization_info) @@ -1244,7 +1343,7 @@ def test_cloud_client_list_organizations_reports_ambiguity_candidates( monkeypatch.setattr( api_util, "get_user_by_auth_id", - lambda *_, **__: {"userId": "user-id"}, + lambda *_, **__: _user_response(), ) monkeypatch.setattr( api_util, @@ -1411,15 +1510,22 @@ def test_cloud_client_list_workspaces_resolves_single_membership_and_caches_it( captured = _patch_workspace_discovery(monkeypatch, permissions=permissions) calls = {"user": 0, "permissions": 0} - def fake_get_user_by_auth_id(*_: object, **__: object) -> dict[str, object]: + def fake_get_user_by_auth_id(*_: object, **__: object) -> UserRead: calls["user"] += 1 - return {"userId": "user-id"} + return _user_response() def fake_list_permissions_for_user( *_: object, **__: object - ) -> list[dict[str, object]]: + ) -> list[PermissionRead]: calls["permissions"] += 1 - return permissions + return [ + _permission_response( + organization_id=str(permission["organizationId"]) + if permission.get("organizationId") is not None + else None + ) + for permission in permissions + ] monkeypatch.setattr(api_util, "get_user_by_auth_id", fake_get_user_by_auth_id) monkeypatch.setattr( @@ -1450,10 +1556,10 @@ def test_cloud_client_list_workspaces_rejects_ambiguous_memberships_with_candida ], ) - def fake_get_organization_info(**kwargs: object) -> dict[str, object]: + def fake_get_organization_info(**kwargs: object) -> OrganizationInfoRead: if kwargs["organization_id"] == "organization-2": raise AirbyteError(message="Forbidden") - return {"organizationName": "Organization 1"} + return _organization_info_response(organization_name="Organization 1") monkeypatch.setattr(api_util, "get_organization_info", fake_get_organization_info) From 6a1eb17f78e323785d68fc8476f58e396554e6ee Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:07:51 +0000 Subject: [PATCH 03/11] refactor(cloud): drop getattr in organization info accessors Co-Authored-By: AJ Steers --- airbyte/cloud/organizations.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/airbyte/cloud/organizations.py b/airbyte/cloud/organizations.py index 9aec6f688..6777dfa93 100644 --- a/airbyte/cloud/organizations.py +++ b/airbyte/cloud/organizations.py @@ -96,27 +96,25 @@ def organization_name(self) -> str | None: if self._organization_name is not None: return self._organization_name info = self._fetch_organization_info() - return getattr(info, "organizationName", None) + return info.organizationName if info is not None else None @property def email(self) -> str | None: """Email associated with the organization.""" - if self._email is not None: - return self._email return self._email @property def payment_status(self) -> str | None: """Payment status of the organization.""" info = self._fetch_organization_info() - billing = getattr(info, "billing", None) + billing = info.billing if info is not None else None return billing.paymentStatus.value if billing is not None else None @property def subscription_status(self) -> str | None: """Subscription status of the organization.""" info = self._fetch_organization_info() - billing = getattr(info, "billing", None) + billing = info.billing if info is not None else None return ( billing.subscriptionStatus.value if billing is not None and billing.subscriptionStatus is not None From d239ce96fcac194c89cfc73a8880928b89e59a51 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:11:39 +0000 Subject: [PATCH 04/11] ci: authenticate private airbyte-ops-mcp git dep via Octavia bot app Co-Authored-By: AJ Steers --- .../actions/private-git-deps-auth/action.yml | 29 +++++++++++++++++++ .github/workflows/autofix.yml | 5 ++++ .github/workflows/fix-pr-command.yml | 5 ++++ .github/workflows/pydoc_preview.yml | 5 ++++ .github/workflows/pydoc_publish.yml | 5 ++++ .github/workflows/python_lint.yml | 20 +++++++++++++ .github/workflows/python_pytest.yml | 22 ++++++++++++++ .github/workflows/test-pr-command.yml | 5 ++++ airbyte/_util/api_util.py | 5 ++++ airbyte/cloud/sync_results.py | 4 +++ pyproject.toml | 1 + 11 files changed, 106 insertions(+) create mode 100644 .github/actions/private-git-deps-auth/action.yml diff --git a/.github/actions/private-git-deps-auth/action.yml b/.github/actions/private-git-deps-auth/action.yml new file mode 100644 index 000000000..0b3fa5ab1 --- /dev/null +++ b/.github/actions/private-git-deps-auth/action.yml @@ -0,0 +1,29 @@ +# TK-TODO: Temporary. `airbyte-server-models` is consumed from the private airbyte-ops-mcp repo +# until it is published to PyPI (airbytehq/airbyte-ops-mcp#1326). Remove this action and its +# call sites once pyproject.toml pins a released version. +name: Authenticate git for private dependency repos +description: Authenticate git for private dependency repos +inputs: + app-id: + description: GitHub App ID. + required: true + private-key: + description: GitHub App private key. + required: true +runs: + using: composite + steps: + - name: Create GitHub App token + id: get-app-token + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + with: + owner: "airbytehq" + repositories: "airbyte-ops-mcp" + app-id: ${{ inputs.app-id }} + private-key: ${{ inputs.private-key }} + - name: Configure git credentials for airbyte-ops-mcp + shell: bash + env: + GH_APP_TOKEN: ${{ steps.get-app-token.outputs.token }} + run: | + git config --global url."https://x-access-token:${GH_APP_TOKEN}@github.com/airbytehq/airbyte-ops-mcp".insteadOf "https://github.com/airbytehq/airbyte-ops-mcp" diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 2159659ee..f3c529c2c 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -23,6 +23,11 @@ jobs: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev diff --git a/.github/workflows/fix-pr-command.yml b/.github/workflows/fix-pr-command.yml index ff91cd6f7..210e1f07f 100644 --- a/.github/workflows/fix-pr-command.yml +++ b/.github/workflows/fix-pr-command.yml @@ -86,6 +86,11 @@ jobs: with: version: "latest" python-version: ${{ matrix.python-version }} + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev diff --git a/.github/workflows/pydoc_preview.yml b/.github/workflows/pydoc_preview.yml index cba9061c8..80218fe30 100644 --- a/.github/workflows/pydoc_preview.yml +++ b/.github/workflows/pydoc_preview.yml @@ -25,6 +25,11 @@ jobs: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev diff --git a/.github/workflows/pydoc_publish.yml b/.github/workflows/pydoc_publish.yml index db8198f4c..f033ed894 100644 --- a/.github/workflows/pydoc_publish.yml +++ b/.github/workflows/pydoc_publish.yml @@ -41,6 +41,11 @@ jobs: - name: Setup Pages uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev diff --git a/.github/workflows/python_lint.yml b/.github/workflows/python_lint.yml index 8cddeeebe..f62280b92 100644 --- a/.github/workflows/python_lint.yml +++ b/.github/workflows/python_lint.yml @@ -24,6 +24,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -44,6 +49,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -64,6 +74,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -86,6 +101,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev diff --git a/.github/workflows/python_pytest.yml b/.github/workflows/python_pytest.yml index bf44d0201..53d0a2189 100644 --- a/.github/workflows/python_pytest.yml +++ b/.github/workflows/python_pytest.yml @@ -44,6 +44,12 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + if: github.event.pull_request.head.repo.fork == false + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -118,6 +124,12 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + if: github.event.pull_request.head.repo.fork == false + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -206,6 +218,11 @@ jobs: with: version: "latest" python-version: ${{ matrix.python-version }} + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -286,6 +303,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev diff --git a/.github/workflows/test-pr-command.yml b/.github/workflows/test-pr-command.yml index 338b7ef2e..07e01043e 100644 --- a/.github/workflows/test-pr-command.yml +++ b/.github/workflows/test-pr-command.yml @@ -112,6 +112,11 @@ jobs: with: version: "latest" python-version: ${{ matrix.python-version }} + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev diff --git a/airbyte/_util/api_util.py b/airbyte/_util/api_util.py index 12e3ff90a..f685bccc2 100644 --- a/airbyte/_util/api_util.py +++ b/airbyte/_util/api_util.py @@ -22,6 +22,11 @@ import requests from airbyte_api import api, models from airbyte_api.errors import SDKError + +# The Config API models live in an underscore-prefixed module on purpose: the Config API is +# Airbyte-internal and may change without notice, so the private name is a deliberate signal +# to consumers. PyAirbyte accepts that contract, hence the `PLC2701` (import-private-name) +# suppressions below. from airbyte_server_models._config_api import ( AirbyteCatalog, # noqa: PLC2701 BuilderProjectForDefinitionRequestBody, # noqa: PLC2701 diff --git a/airbyte/cloud/sync_results.py b/airbyte/cloud/sync_results.py index 9865439a5..328e239c8 100644 --- a/airbyte/cloud/sync_results.py +++ b/airbyte/cloud/sync_results.py @@ -105,6 +105,10 @@ from dataclasses import asdict, dataclass from typing import TYPE_CHECKING, Any, final +# The Config API models live in an underscore-prefixed module on purpose: the Config API is +# Airbyte-internal and may change without notice, so the private name is a deliberate signal +# to consumers. PyAirbyte accepts that contract, hence the `PLC2701` (import-private-name) +# suppressions below. from airbyte_server_models._config_api import JobIdRequestBody, JobInfoRead # noqa: PLC2701 from airbyte_cdk.utils.datetime_helpers import ab_datetime_parse diff --git a/pyproject.toml b/pyproject.toml index 5c36fd399..77708cdaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,6 +109,7 @@ allow-direct-references = true # TK-TODO: `airbyte-server-models` is consumed from a git branch while the package is # unreleased. Pin this to a published PyPI version (and remove this source entry) # before the next PyAirbyte release. +# CI authenticates to the private repo via .github/actions/private-git-deps-auth (Octavia bot app). airbyte-server-models = { git = "https://github.com/airbytehq/airbyte-ops-mcp.git", branch = "devin/1788977312-server-models", subdirectory = "airbyte-server-models" } [tool.hatch.build.targets.wheel] From 67111c9895e56bc794c00a921dfea50c73cbd60e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:21:13 +0000 Subject: [PATCH 05/11] fix: resolve ruff and pyrefly findings for py311 target Co-Authored-By: AJ Steers --- airbyte/_message_iterators.py | 3 +-- airbyte/_util/destination_smoke_tests.py | 4 ++-- airbyte/_util/telemetry.py | 8 +++---- airbyte/caches/_state_backend.py | 10 ++++----- airbyte/caches/base.py | 3 +-- airbyte/cloud/models.py | 21 +++++++++++-------- airbyte/mcp/interactive/_shared_models.py | 6 +++--- airbyte/mcp/interactive/_sync_history_ui.py | 8 +++---- .../interactive/_workspace_sync_status_ui.py | 18 ++++++++-------- airbyte/progress.py | 2 +- airbyte/records.py | 6 +++--- airbyte/registry.py | 9 ++++---- airbyte/secrets/base.py | 4 ++-- airbyte/strategies.py | 6 +++--- 14 files changed, 54 insertions(+), 54 deletions(-) diff --git a/airbyte/_message_iterators.py b/airbyte/_message_iterators.py index e6efd5a11..15af59e0b 100644 --- a/airbyte/_message_iterators.py +++ b/airbyte/_message_iterators.py @@ -5,10 +5,9 @@ import sys from collections.abc import Iterator -from typing import IO, TYPE_CHECKING, cast +from typing import IO, TYPE_CHECKING, cast, final import pydantic -from typing_extensions import final from airbyte_cdk.utils.datetime_helpers import ab_datetime_now from airbyte_protocol.models import ( diff --git a/airbyte/_util/destination_smoke_tests.py b/airbyte/_util/destination_smoke_tests.py index 8990d6021..6b142786b 100644 --- a/airbyte/_util/destination_smoke_tests.py +++ b/airbyte/_util/destination_smoke_tests.py @@ -20,7 +20,7 @@ import json import logging import time -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from typing import TYPE_CHECKING, Any @@ -65,7 +65,7 @@ def generate_namespace( default suffix. """ suffix = namespace_suffix or DEFAULT_NAMESPACE_SUFFIX - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) ts = now.strftime("%Y%m%d_%H%M") return f"{NAMESPACE_PREFIX}_{ts}_{suffix}" diff --git a/airbyte/_util/telemetry.py b/airbyte/_util/telemetry.py index ff3df328f..c465a1228 100644 --- a/airbyte/_util/telemetry.py +++ b/airbyte/_util/telemetry.py @@ -35,7 +35,7 @@ import os import sys from contextlib import suppress -from enum import Enum +from enum import StrEnum from functools import lru_cache from pathlib import Path from typing import Any, cast @@ -172,14 +172,14 @@ def _get_analytics_id() -> str | None: _ANALYTICS_ID = _get_analytics_id() -class EventState(str, Enum): +class EventState(StrEnum): STARTED = "started" FAILED = "failed" SUCCEEDED = "succeeded" CANCELED = "canceled" -class EventType(str, Enum): +class EventType(StrEnum): INSTALL = "install" SYNC = "sync" VALIDATE = "validate" @@ -258,7 +258,7 @@ def send_telemetry( "anonymousId": _get_analytics_id(), "event": event_type, "properties": payload_props, - "timestamp": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(), + "timestamp": datetime.datetime.now(tz=datetime.UTC).isoformat(), }, ) diff --git a/airbyte/caches/_state_backend.py b/airbyte/caches/_state_backend.py index c61833d78..7746453db 100644 --- a/airbyte/caches/_state_backend.py +++ b/airbyte/caches/_state_backend.py @@ -4,7 +4,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import TYPE_CHECKING from sqlalchemy import Column, DateTime, PrimaryKeyConstraint, String, and_ @@ -59,8 +59,8 @@ class CacheStreamStateModel(SqlAlchemyModel): # type: ignore[misc] last_updated = Column( DateTime(timezone=True), - onupdate=datetime.now(timezone.utc), - default=datetime.now(timezone.utc), + onupdate=datetime.now(UTC), + default=datetime.now(UTC), ) """The last time the state was updated.""" @@ -91,8 +91,8 @@ class DestinationStreamStateModel(SqlAlchemyModel): # type: ignore[misc] last_updated = Column( DateTime(timezone=True), - onupdate=datetime.now(timezone.utc), - default=datetime.now(timezone.utc), + onupdate=datetime.now(UTC), + default=datetime.now(UTC), ) """The last time the state was updated.""" diff --git a/airbyte/caches/base.py b/airbyte/caches/base.py index 2e31c0ccb..8f8e588be 100644 --- a/airbyte/caches/base.py +++ b/airbyte/caches/base.py @@ -5,7 +5,7 @@ import contextlib from pathlib import Path -from typing import IO, TYPE_CHECKING, Any, ClassVar, Literal, final +from typing import IO, TYPE_CHECKING, Any, ClassVar, Literal, Self, final import pandas as pd import pyarrow as pa @@ -13,7 +13,6 @@ from pydantic import Field, PrivateAttr from sqlalchemy import exc as sqlalchemy_exc from sqlalchemy import text -from typing_extensions import Self from airbyte_protocol.models import ConfiguredAirbyteCatalog diff --git a/airbyte/cloud/models.py b/airbyte/cloud/models.py index d5cf3bef0..4a4a3fa76 100644 --- a/airbyte/cloud/models.py +++ b/airbyte/cloud/models.py @@ -5,7 +5,7 @@ from collections.abc import Mapping from dataclasses import asdict, is_dataclass -from enum import Enum +from enum import Enum, StrEnum from typing import Any, Protocol from pydantic import BaseModel, ConfigDict, Field @@ -15,6 +15,7 @@ class _WorkspaceResponseLike(Protocol): workspace_id: str name: str data_residency: str + organization_id: str | None notifications: object @@ -56,7 +57,7 @@ class _DeclarativeSourceDefinitionResponseLike(Protocol): version: object -class JobStatusEnum(str, Enum): +class JobStatusEnum(StrEnum): """Status values for an Airbyte Cloud job.""" PENDING = "pending" @@ -67,7 +68,7 @@ class JobStatusEnum(str, Enum): CANCELLED = "cancelled" -class JobTypeEnum(str, Enum): +class JobTypeEnum(StrEnum): """Job type values for Airbyte Cloud jobs.""" SYNC = "sync" @@ -101,12 +102,14 @@ class CloudWorkspaceInfo(BaseModel): @classmethod def from_api_response(cls, workspace: _WorkspaceResponseLike) -> CloudWorkspaceInfo: """Create a public model from an internal API workspace response.""" - return cls( - workspace_id=workspace.workspace_id, - name=workspace.name, - data_residency=workspace.data_residency, - organization_id=getattr(workspace, "organization_id", None), - notifications=_notifications_to_dict(workspace.notifications), + return cls.model_validate( + { + "workspaceId": workspace.workspace_id, + "name": workspace.name, + "dataResidency": workspace.data_residency, + "organizationId": getattr(workspace, "organization_id", None), + "notifications": _notifications_to_dict(workspace.notifications), + } ) @classmethod diff --git a/airbyte/mcp/interactive/_shared_models.py b/airbyte/mcp/interactive/_shared_models.py index 90a0f22d1..c09cc5119 100644 --- a/airbyte/mcp/interactive/_shared_models.py +++ b/airbyte/mcp/interactive/_shared_models.py @@ -3,12 +3,12 @@ from __future__ import annotations -from enum import Enum +from enum import StrEnum from pydantic import BaseModel, Field -class SupportLevel(str, Enum): +class SupportLevel(StrEnum): """Connector support levels ordered by precedence.""" ARCHIVED = "archived" @@ -44,7 +44,7 @@ def parse(cls, value: str) -> SupportLevel: ) from None -class ConnectorType(str, Enum): +class ConnectorType(StrEnum): """Connector type: `source` or `destination`.""" SOURCE = "source" diff --git a/airbyte/mcp/interactive/_sync_history_ui.py b/airbyte/mcp/interactive/_sync_history_ui.py index 72ba03467..5cefb65c6 100644 --- a/airbyte/mcp/interactive/_sync_history_ui.py +++ b/airbyte/mcp/interactive/_sync_history_ui.py @@ -361,8 +361,8 @@ def _build_sync_history_app( # noqa: PLR0913 BarChart( data=chart_data, series=[ - ChartSeries(data_key="succeeded", label="Succeeded", color="#22c55e"), - ChartSeries(data_key="failed", label="Failed", color="#ef4444"), + ChartSeries(dataKey="succeeded", label="Succeeded", color="#22c55e"), + ChartSeries(dataKey="failed", label="Failed", color="#ef4444"), ], x_axis="time", stacked=True, @@ -374,7 +374,7 @@ def _build_sync_history_app( # noqa: PLR0913 LineChart( data=chart_data, series=[ - ChartSeries(data_key="records", label="Records", color="#3b82f6"), + ChartSeries(dataKey="records", label="Records", color="#3b82f6"), ], x_axis="time", height=280, @@ -387,7 +387,7 @@ def _build_sync_history_app( # noqa: PLR0913 LineChart( data=chart_data, series=[ - ChartSeries(data_key="bytes", label="Bytes", color="#8b5cf6"), + ChartSeries(dataKey="bytes", label="Bytes", color="#8b5cf6"), ], x_axis="time", height=280, diff --git a/airbyte/mcp/interactive/_workspace_sync_status_ui.py b/airbyte/mcp/interactive/_workspace_sync_status_ui.py index 77d7ca451..397136c1b 100644 --- a/airbyte/mcp/interactive/_workspace_sync_status_ui.py +++ b/airbyte/mcp/interactive/_workspace_sync_status_ui.py @@ -5,7 +5,7 @@ import json from dataclasses import asdict, dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from enum import Enum from typing import TYPE_CHECKING, Annotated, Literal @@ -193,7 +193,7 @@ def show_workspace_sync_status( """Show an interactive sync status dashboard for an Airbyte Cloud workspace.""" workspace: CloudWorkspace = _get_cloud_workspace(ctx, workspace_id) connections = workspace.list_connections(limit=max_connections) - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) connection_statuses = [ _summarize_connection( connection=connection, @@ -334,7 +334,7 @@ def _build_workspace_metric_summary( continue latest_sync_time = datetime.fromisoformat(connection_status.latest_sync_time) if latest_sync_time.tzinfo is None: - latest_sync_time = latest_sync_time.replace(tzinfo=timezone.utc) + latest_sync_time = latest_sync_time.replace(tzinfo=UTC) age_hours = (now - latest_sync_time).total_seconds() / 3600 if age_hours <= recent_hours: recently_synced += 1 @@ -650,14 +650,14 @@ def _status_pie_section( with Div(style=_status_pie_chart_style(status_pie_rows)): PieChart( data=status_pie_rows, - data_key="connections", - name_key="status", + dataKey="connections", + nameKey="status", height=360, - inner_radius=87, + innerRadius=87, padding_angle=2, - show_label=True, - show_legend=True, - show_tooltip=True, + showLabel=True, + showLegend=True, + showTooltip=True, ) with Row(gap=3, css_class="flex-wrap"): for status_row in status_pie_rows: diff --git a/airbyte/progress.py b/airbyte/progress.py index ad984f8a3..26110e1d2 100644 --- a/airbyte/progress.py +++ b/airbyte/progress.py @@ -115,7 +115,7 @@ def _to_time_str(timestamp: float) -> str: For now, we'll just use UTC to avoid breaking tests. In the future, we should return a local time string. """ - datetime_obj = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc) + datetime_obj = datetime.datetime.fromtimestamp(timestamp, tz=datetime.UTC) datetime_obj = datetime_obj.astimezone() return datetime_obj.strftime("%H:%M:%S") diff --git a/airbyte/records.py b/airbyte/records.py index e28109088..1fc5a263c 100644 --- a/airbyte/records.py +++ b/airbyte/records.py @@ -68,7 +68,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import TYPE_CHECKING, Any from uuid_extensions import uuid7str @@ -235,7 +235,7 @@ def __init__( self.update( { AB_RAW_ID_COLUMN: uuid7str(), - AB_EXTRACTED_AT_COLUMN: extracted_at or datetime.now(timezone.utc), + AB_EXTRACTED_AT_COLUMN: extracted_at or datetime.now(UTC), AB_META_COLUMN: {}, } ) @@ -253,7 +253,7 @@ def from_record_message( from_dict=data_dict, stream_record_handler=stream_record_handler, with_internal_columns=True, - extracted_at=datetime.fromtimestamp(record_message.emitted_at / 1000, tz=timezone.utc), + extracted_at=datetime.fromtimestamp(record_message.emitted_at / 1000, tz=UTC), ) def __getitem__(self, key: str) -> Any: # noqa: ANN401 diff --git a/airbyte/registry.py b/airbyte/registry.py index 6f465ded0..57bbf7845 100644 --- a/airbyte/registry.py +++ b/airbyte/registry.py @@ -8,14 +8,13 @@ import os import warnings from copy import copy -from enum import Enum +from enum import StrEnum from pathlib import Path -from typing import Any, cast +from typing import Any, Self, cast import requests import yaml from pydantic import BaseModel, Field -from typing_extensions import Self from airbyte import exceptions as exc from airbyte._registry_utils import fetch_registry_version_date, parse_changelog_html @@ -45,7 +44,7 @@ ) -class InstallType(str, Enum): +class InstallType(StrEnum): """The type of installation for a connector.""" YAML = "yaml" @@ -66,7 +65,7 @@ class InstallType(str, Enum): """All connectors in the registry (environment-independent).""" -class Language(str, Enum): +class Language(StrEnum): """The language of a connector.""" PYTHON = InstallType.PYTHON.value diff --git a/airbyte/secrets/base.py b/airbyte/secrets/base.py index aa0ba911e..b5f9d2675 100644 --- a/airbyte/secrets/base.py +++ b/airbyte/secrets/base.py @@ -5,7 +5,7 @@ import json from abc import ABC, abstractmethod -from enum import Enum +from enum import StrEnum from typing import TYPE_CHECKING, Any, cast from pydantic_core import CoreSchema, core_schema @@ -20,7 +20,7 @@ from pydantic.json_schema import JsonSchemaValue -class SecretSourceEnum(str, Enum): +class SecretSourceEnum(StrEnum): """Enumeration of secret sources supported by PyAirbyte.""" ENV = "env" diff --git a/airbyte/strategies.py b/airbyte/strategies.py index e55b4d9a8..023d0b683 100644 --- a/airbyte/strategies.py +++ b/airbyte/strategies.py @@ -4,7 +4,7 @@ from __future__ import annotations -from enum import Enum +from enum import StrEnum from airbyte_protocol.models import DestinationSyncMode @@ -15,7 +15,7 @@ _AUTO = "auto" -class WriteStrategy(str, Enum): +class WriteStrategy(StrEnum): """Read strategies for PyAirbyte. Read strategies set a preferred method for writing data to a destination. The actual method used @@ -51,7 +51,7 @@ class WriteStrategy(str, Enum): """ -class WriteMethod(str, Enum): +class WriteMethod(StrEnum): """Write methods for PyAirbyte. Unlike write strategies, write methods are expected to be fully resolved and do not require any From 9ce62441290d14e82b22c31f6eff8ecd2727aa3f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:21:18 +0000 Subject: [PATCH 06/11] fix(cloud): wrap invalid JSON Config API responses in AirbyteError Co-Authored-By: AJ Steers --- airbyte/_util/api_util.py | 2 +- tests/unit_tests/test_cloud_api_util.py | 40 +++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/airbyte/_util/api_util.py b/airbyte/_util/api_util.py index f685bccc2..3e5e3c71e 100644 --- a/airbyte/_util/api_util.py +++ b/airbyte/_util/api_util.py @@ -1955,7 +1955,7 @@ def _make_config_api_request( try: return response_model.model_validate(response.json()) - except ValidationError as ex: + except (ValidationError, requests.exceptions.JSONDecodeError) as ex: raise AirbyteError( message=f"Config API response for {path} did not match the expected schema.", context={ diff --git a/tests/unit_tests/test_cloud_api_util.py b/tests/unit_tests/test_cloud_api_util.py index 7a6f55469..37b7a4a24 100644 --- a/tests/unit_tests/test_cloud_api_util.py +++ b/tests/unit_tests/test_cloud_api_util.py @@ -18,20 +18,19 @@ from airbyte.secrets.base import SecretString from airbyte_api import api, models from airbyte_server_models._config_api import ( + CheckConnectionRead, + JobConfigType, OrganizationInfoRead, OrganizationRead, OrganizationReadList, PermissionRead, PermissionReadList, PermissionType, - CheckConnectionRead, - JobConfigType, Status4, SynchronousJobRead, UserRead, ) - USER_ID = UUID("00000000-0000-0000-0000-000000000001") ORGANIZATION_ID = UUID("00000000-0000-0000-0000-000000000002") PERMISSION_ID = UUID("00000000-0000-0000-0000-000000000003") @@ -262,6 +261,41 @@ def test_make_config_api_request_wraps_response_validation_error( } +def test_make_config_api_request_wraps_invalid_json_response( + monkeypatch: pytest.MonkeyPatch, +) -> None: + response = requests.Response() + response.status_code = 200 + response._content = b"not json" + response.url = "https://config.example/users/get_by_auth_id" + + def raise_json_error() -> object: + raise requests.exceptions.JSONDecodeError("x", "doc", 0) + + monkeypatch.setattr(response, "json", raise_json_error) + monkeypatch.setattr(requests, "request", lambda **_: response) + + with pytest.raises( + AirbyteError, match="did not match the expected schema" + ) as exc_info: + api_util._make_config_api_request( + api_root="https://api.example", + path="/users/get_by_auth_id", + request=api_util.UserAuthIdRequestBody(authUserId="auth-user-id"), + response_model=UserRead, + client_id=None, + client_secret=None, + bearer_token=SecretString("token"), + config_api_root="https://config.example", + ) + + assert exc_info.value.context == { + "full_url": "https://config.example/users/get_by_auth_id", + "path": "/users/get_by_auth_id", + "response": "not json", + } + + def test_get_organization_info_forwards_typed_request( monkeypatch: pytest.MonkeyPatch, ) -> None: From 1f3a9a10c81254e921d514ddfb9e79a247957378 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:34:22 +0000 Subject: [PATCH 07/11] feat!: replace Speakeasy airbyte-api SDK with generated airbyte-server-models public_api Co-Authored-By: AJ Steers --- airbyte/_util/api_imports.py | 10 +- airbyte/_util/api_util.py | 1386 ++++++++--------- airbyte/caches/_utils/_cache_to_dest.py | 110 +- airbyte/caches/_utils/_dest_to_cache.py | 170 +- airbyte/caches/bigquery.py | 7 +- airbyte/caches/duckdb.py | 6 +- airbyte/caches/motherduck.py | 6 +- airbyte/caches/postgres.py | 26 +- airbyte/caches/snowflake.py | 7 +- airbyte/cloud/client.py | 44 +- airbyte/cloud/connectors.py | 27 +- airbyte/cloud/models.py | 47 +- airbyte/cloud/organizations.py | 9 +- airbyte/cloud/workspaces.py | 3 +- airbyte/registry.py | 1 - pyproject.toml | 3 +- tests/integration_tests/cloud/conftest.py | 10 +- .../cloud/test_cloud_api_util.py | 24 +- tests/unit_tests/test_cache_to_dest.py | 121 ++ tests/unit_tests/test_cloud_api_util.py | 1020 ++++++------ tests/unit_tests/test_cloud_client.py | 28 +- tests/unit_tests/test_cloud_connections.py | 31 +- tests/unit_tests/test_cloud_credentials.py | 88 +- tests/unit_tests/test_cloud_models.py | 14 +- tests/unit_tests/test_public_api_imports.py | 15 +- uv.lock | 89 +- 26 files changed, 1547 insertions(+), 1755 deletions(-) create mode 100644 tests/unit_tests/test_cache_to_dest.py diff --git a/airbyte/_util/api_imports.py b/airbyte/_util/api_imports.py index df367256e..f92e3f587 100644 --- a/airbyte/_util/api_imports.py +++ b/airbyte/_util/api_imports.py @@ -6,9 +6,9 @@ for type hinting purposes. Design Guidelines: -- No modules except `api_util` and `api_imports` should import from `airbyte_api`. -- If a module needs to import from `airbyte_api`, it should import from `api_imports` (this module) - instead. +- No modules except `api_util` and `api_imports` should import from `airbyte_server_models`. +- If a module needs to import from `airbyte_server_models`, it should import from + `api_imports` (this module) instead. - This module is divided into two sections: internal-use classes and public-use classes. - Public-use classes should be carefully reviewed to ensure that they are necessary for public use and that we are willing to support them as part of PyAirbyte. @@ -21,7 +21,7 @@ # Internal-Use Classes # These classes are used internally to cache API responses. -from airbyte_api.models import ( +from airbyte_server_models.public_api import ( ConnectionResponse, DestinationResponse, JobResponse, @@ -32,7 +32,7 @@ # This class is used to represent the status of a job. It may be used in # type hints for public functions that return a job status. -from airbyte_api.models import JobStatusEnum # Alias not needed +from airbyte_server_models.public_api import JobStatusEnum # Alias not needed __all__: list[str] = [ diff --git a/airbyte/_util/api_util.py b/airbyte/_util/api_util.py index de77942f0..382ff71f2 100644 --- a/airbyte/_util/api_util.py +++ b/airbyte/_util/api_util.py @@ -6,22 +6,21 @@ PyAirbyte. Classes and functions from the Airbyte API external library should always be wrapped in PyAirbyte classes - unless there's a very compelling reason to surface these models intentionally. -Similarly, modules outside of this file should try to avoid interfacing with `airbyte_api` library -directly. This will ensure a single source of truth when mapping between the `airbyte` and -`airbyte_api` libraries. +Similarly, modules outside of this file should try to avoid interfacing with +`airbyte_server_models` directly. This will ensure a single source of truth when mapping +between the `airbyte` and `airbyte_server_models` libraries. """ from __future__ import annotations import base64 import json +import logging from http import HTTPStatus -from typing import TYPE_CHECKING, Any, Literal, TypeVar +from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast, overload -import airbyte_api import requests -from airbyte_api import api, models -from airbyte_api.errors import SDKError +from airbyte_server_models import public_api as models # The Config API models live in an underscore-prefixed module on purpose: the Config API is # Airbyte-internal and may change without notice, so the private name is a deliberate signal @@ -83,9 +82,8 @@ if TYPE_CHECKING: from collections.abc import Callable - from airbyte_api.models import ( - DestinationConfiguration, - ) + +logger = logging.getLogger(__name__) JOB_WAIT_INTERVAL_SECS = 2.0 @@ -127,39 +125,34 @@ def _get_page_limit(remaining: int | None) -> int: return min(remaining, PAGE_SIZE) -def _get_sdk_error_context(error: SDKError) -> dict[str, Any]: - """Extract context information from an SDKError for debugging. +def _get_api_error_context(response: requests.Response) -> dict[str, Any]: + """Extract request and response details from a failed API call for debugging. - This helper extracts the actual request URL and other useful debugging - information from the Speakeasy SDK's SDKError exception. The SDK stores - the raw response object which contains the request details. + Produces the same context keys the Speakeasy SDK error wrapper used to surface. """ context: dict[str, Any] = { - "status_code": error.status_code, - "error_message": error.message, + "status_code": response.status_code, + "error_message": response.text, } - - if error.raw_response is not None: - request = error.raw_response.request - if request is not None: - context["request_url"] = str(request.url) - context["request_method"] = request.method - context["response_content_type"] = error.raw_response.headers.get("content-type") - + if response.request is not None: + context["request_url"] = str(response.request.url) + context["request_method"] = response.request.method + context["response_content_type"] = response.headers.get("content-type") return context -def _wrap_sdk_error( - error: SDKError, base_context: dict[str, Any] | None = None +def _wrap_api_error( + response: requests.Response, base_context: dict[str, Any] | None = None ) -> AirbyteCloudError: - """Wrap an SDKError with additional context for debugging. + """Wrap a failed API response with additional context for debugging. - This function converts a Speakeasy SDK error into an AirbyteCloudError with - full URL context, making it easier to debug API issues like 403 and 404 errors. + Mirrors the former SDK error-wrapping semantics: 403 and 404 become + `AirbyteMissingResourceError` (403 with extra guidance), everything else + becomes `AirbyteCloudError`. """ - sdk_context = _get_sdk_error_context(error) - merged_context = {**(base_context or {}), **sdk_context} - status_code = sdk_context.get("status_code") + api_context = _get_api_error_context(response) + merged_context = {**(base_context or {}), **api_context} + status_code = api_context.get("status_code") is_forbidden = status_code == HTTPStatus.FORBIDDEN error_type = ( AirbyteMissingResourceError @@ -171,7 +164,7 @@ def _wrap_sdk_error( "The requested resource was not found, or these credentials can't access it " "(HTTP 403)." if is_forbidden - else f"API error occurred: {error.message}" + else f"API error occurred: {response.text}" ), guidance=FORBIDDEN_RESOURCE_GUIDANCE if is_forbidden else None, context=merged_context, @@ -253,30 +246,19 @@ def get_web_url_root(api_root: str) -> str: return api_root -def get_airbyte_server_instance( +def _resolve_bearer_token( *, api_root: str, client_id: SecretString | None, client_secret: SecretString | None, bearer_token: SecretString | None, -) -> airbyte_api.AirbyteAPI: - """Get an Airbyte API instance. + timeout: tuple[float, float] | None = None, +) -> SecretString: + """Resolve the bearer token to use, minting one from client credentials if needed. Supports two authentication methods (mutually exclusive): 1. OAuth2 client credentials (client_id + client_secret) 2. Bearer token authentication - - Args: - api_root: The API root URL. - client_id: OAuth2 client ID (required if not using bearer_token). - client_secret: OAuth2 client secret (required if not using bearer_token). - bearer_token: Pre-generated bearer token (alternative to client credentials). - - Returns: - An authenticated AirbyteAPI instance. - - Raises: - AirbyteLibInputError: If authentication parameters are invalid. """ # Guard: must provide either bearer token OR both client credentials if bearer_token is None and (client_id is None or client_secret is None): @@ -292,33 +274,170 @@ def get_airbyte_server_instance( guidance="Provide either client_id and client_secret, or bearer_token, but not both.", ) - client = requests.Session() - client.headers[AIRBYTE_ANALYTIC_SOURCE_HEADER] = get_cloud_api_analytic_source() - - # Option 1: Bearer token authentication - if bearer_token is not None: - return airbyte_api.AirbyteAPI( - security=models.Security( - bearer_auth=bearer_token, - ), - server_url=api_root, - client=client, + if bearer_token is None: + # Client credentials flow (guaranteed non-None by first guard) + bearer_token = get_bearer_token( + client_id=cast(SecretString, client_id), + client_secret=cast(SecretString, client_secret), + api_root=api_root, + timeout=timeout, ) + return bearer_token - # Option 2: Client credentials flow (guaranteed non-None by first guard) - return airbyte_api.AirbyteAPI( - security=models.Security( - client_credentials=models.SchemeClientCredentials( - client_id=client_id, - client_secret=client_secret, - token_url=api_root + "/applications/token", - # e.g. https://api.airbyte.com/v1/applications/token - ), +def _api_headers( + *, + api_root: str, + client_id: SecretString | None, + client_secret: SecretString | None, + bearer_token: SecretString | None, +) -> dict[str, str]: + """Build Public API headers, minting a bearer token from client credentials if needed.""" + resolved_token = _resolve_bearer_token( + api_root=api_root, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + ) + return { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": f"Bearer {resolved_token}", + "User-Agent": "PyAirbyte Client", + AIRBYTE_ANALYTIC_SOURCE_HEADER: get_cloud_api_analytic_source(), + } + + +@overload +def _make_public_api_request( # Mirrors the API surface. + *, + method: Literal["GET", "POST", "PATCH", "PUT", "DELETE"], + api_root: str, + path: str, + response_model: type[_T], + client_id: SecretString | None, + client_secret: SecretString | None, + bearer_token: SecretString | None, + request: BaseModel | None = None, + params: dict[str, Any] | None = None, + error_context: dict[str, Any] | None = None, +) -> _T | None: ... + + +@overload +def _make_public_api_request( # Mirrors the API surface. + *, + method: Literal["GET", "POST", "PATCH", "PUT", "DELETE"], + api_root: str, + path: str, + response_model: None, + client_id: SecretString | None, + client_secret: SecretString | None, + bearer_token: SecretString | None, + request: BaseModel | None = None, + params: dict[str, Any] | None = None, + error_context: dict[str, Any] | None = None, +) -> dict[str, Any] | None: ... + + +def _make_public_api_request( # noqa: PLR0913 # Mirrors the API surface. + *, + method: Literal["GET", "POST", "PATCH", "PUT", "DELETE"], + api_root: str, + path: str, + response_model: type[_T] | None, + client_id: SecretString | None, + client_secret: SecretString | None, + bearer_token: SecretString | None, + request: BaseModel | None = None, + params: dict[str, Any] | None = None, + error_context: dict[str, Any] | None = None, +) -> _T | None: + """Make a Public API request and decode the response into `response_model`. + + Raises `AirbyteMissingResourceError` on 403/404 and `AirbyteCloudError` on + other non-2xx responses, matching the former Speakeasy SDK error wrapping. + Returns `None` when the response has no content. + """ + headers = _api_headers( + api_root=api_root, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + ) + full_url = api_root.rstrip("/") + path + response = requests.request( + method=method, + url=full_url, + headers=headers, + params=params, + json=( + request.model_dump(mode="json", by_alias=True, exclude_none=True) + if request is not None + else None ), - server_url=api_root, - client=client, + # No timeout, matching the former SDK behavior. ) + if not status_ok(response.status_code): + raise _wrap_api_error(response, error_context) + + if response.status_code == HTTPStatus.NO_CONTENT or not response.content: + return None + + try: + body = response.json() + except requests.exceptions.JSONDecodeError as ex: + raise AirbyteCloudError( + message=f"Public API response for {path} did not match the expected schema.", + context={ + **(error_context or {}), + "full_url": full_url, + "path": path, + "response": response.text, + }, + ) from ex + + if response_model is None: + return body + + try: + return response_model.model_validate(body) + except ValidationError as ex: + raise AirbyteCloudError( + message=f"Public API response for {path} did not match the expected schema.", + context={ + **(error_context or {}), + "full_url": full_url, + "path": path, + "response": response.text, + }, + ) from ex + + +def _decode_list_items( + body: Any, # noqa: ANN401 # Raw JSON payload + *, + item_model: type[_T], + list_path: str, +) -> list[_T]: + """Decode each item in a list endpoint's `data` payload individually. + + A single malformed item is skipped with a warning instead of failing the + whole page, so one bad record can't take down a listing. + """ + items: list[_T] = [] + for item in (body or {}).get("data") or []: + try: + items.append(item_model.model_validate(item)) + except ValidationError: + item_id = item.get("id") if isinstance(item, dict) else getattr(item, "id", None) + logger.warning( + "Skipping %s item that failed schema validation on %s (id=%s).", + getattr(item_model, "__name__", str(item_model)), + list_path, + item_id, + ) + return items # Get workspace @@ -333,32 +452,24 @@ def get_workspace( bearer_token: SecretString | None, ) -> models.WorkspaceResponse: """Get a workspace object.""" - airbyte_instance = get_airbyte_server_instance( + base_context = {"workspace_id": workspace_id, "api_root": api_root} + workspace = _make_public_api_request( + method="GET", api_root=api_root, + path=f"/workspaces/{workspace_id}", + response_model=models.WorkspaceResponse, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, + error_context=base_context, ) - base_context = {"workspace_id": workspace_id, "api_root": api_root} - try: - response = airbyte_instance.workspaces.get_workspace( - api.GetWorkspaceRequest( - workspace_id=workspace_id, - ), - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e - if status_ok(response.status_code) and response.workspace_response: - return response.workspace_response + if workspace: + return workspace raise AirbyteMissingResourceError( resource_type="workspace", - context={ - "workspace_id": workspace_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, + context=base_context, ) @@ -373,40 +484,34 @@ def create_workspace( region_id: str | None = None, ) -> models.WorkspaceResponse: """Create a workspace.""" - airbyte_instance = get_airbyte_server_instance( - api_root=api_root, - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - ) base_context = { "name": name, "organization_id": organization_id, "region_id": region_id, "api_root": api_root, } - try: - response = airbyte_instance.workspaces.create_workspace( - request=models.WorkspaceCreateRequest( - name=name, - organization_id=organization_id, - region_id=region_id, - ) - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e + workspace = _make_public_api_request( + method="POST", + api_root=api_root, + path="/workspaces", + request=models.WorkspaceCreateRequest( + name=name, + organization_id=organization_id, + region_id=region_id, + ), + response_model=models.WorkspaceResponse, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + error_context=base_context, + ) - if status_ok(response.status_code) and response.workspace_response: - return response.workspace_response + if workspace: + return workspace raise AirbyteCloudError( message="Could not create workspace.", - context={ - **base_context, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - response=response, + context=base_context, ) @@ -420,38 +525,29 @@ def rename_workspace( bearer_token: SecretString | None, ) -> models.WorkspaceResponse: """Rename a workspace.""" - airbyte_instance = get_airbyte_server_instance( - api_root=api_root, - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - ) base_context = { "workspace_id": workspace_id, "name": name, "api_root": api_root, } - try: - response = airbyte_instance.workspaces.update_workspace( - api.UpdateWorkspaceRequest( - workspace_id=workspace_id, - workspace_update_request=models.WorkspaceUpdateRequest(name=name), - ) - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e + workspace = _make_public_api_request( + method="PATCH", + api_root=api_root, + path=f"/workspaces/{workspace_id}", + request=models.WorkspaceUpdateRequest(name=name), + response_model=models.WorkspaceResponse, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + error_context=base_context, + ) - if status_ok(response.status_code) and response.workspace_response: - return response.workspace_response + if workspace: + return workspace raise AirbyteCloudError( message="Could not rename workspace.", - context={ - **base_context, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - response=response, + context=base_context, ) @@ -521,32 +617,19 @@ def permanently_delete_workspace( connection_ids=[connection.connection_id for connection in connections], ) - airbyte_instance = get_airbyte_server_instance( + _make_public_api_request( + method="DELETE", + api_root=api_root, + path=f"/workspaces/{workspace_id}", + response_model=None, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, + error_context={ + "workspace_id": workspace_id, + "api_root": api_root, + }, ) - base_context = { - "workspace_id": workspace_id, - "api_root": api_root, - } - try: - response = airbyte_instance.workspaces.delete_workspace( - api.DeleteWorkspaceRequest(workspace_id=workspace_id), - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e - - if not status_ok(response.status_code): - raise AirbyteCloudError( - context={ - **base_context, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - response=response, - ) # List resources @@ -569,38 +652,28 @@ def list_connections( _validate_pagination_params(limit=limit) name_filter = (lambda n: n == name) if name is not None else name_filter or (lambda _: True) - airbyte_instance = get_airbyte_server_instance( - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - api_root=api_root, - ) result: list[models.ConnectionResponse] = [] current_offset = 0 remaining = limit - base_context = {"workspace_id": workspace_id, "api_root": api_root} while remaining is None or remaining > 0: - try: - response = airbyte_instance.connections.list_connections( - api.ListConnectionsRequest( - workspace_ids=[workspace_id], - offset=current_offset, - limit=PAGE_SIZE, - ), - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e - - if not status_ok(response.status_code): - raise AirbyteCloudError( - context={ - "workspace_id": workspace_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) - assert response.connections_response is not None - page_data = response.connections_response.data + body = _make_public_api_request( + method="GET", + api_root=api_root, + path="/connections", + params={ + "workspaceIds": [workspace_id], + "offset": current_offset, + "limit": PAGE_SIZE, + }, + response_model=None, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + error_context={"workspace_id": workspace_id, "api_root": api_root}, + ) + page_data = _decode_list_items( + body, item_model=models.ConnectionResponse, list_path="/connections" + ) if not page_data: break @@ -614,7 +687,7 @@ def list_connections( if remaining is not None: remaining -= len(page_results) - if not response.connections_response.next: + if not (body or {}).get("next"): break current_offset += len(page_data) @@ -650,39 +723,28 @@ def list_workspaces( has_name_filter = name is not None or name_filter is not None name_filter = (lambda n: n == name) if name is not None else name_filter or (lambda _: True) - airbyte_instance: airbyte_api.AirbyteAPI = get_airbyte_server_instance( - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - api_root=api_root, - ) result: list[models.WorkspaceResponse] = [] current_offset = 0 remaining = limit - base_context = {"workspace_id": workspace_id, "api_root": api_root} while remaining is None or remaining > 0: page_limit = PAGE_SIZE if has_name_filter else _get_page_limit(remaining) - try: - response: api.ListWorkspacesResponse = airbyte_instance.workspaces.list_workspaces( - api.ListWorkspacesRequest( - offset=current_offset, - limit=page_limit, - ), - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e - - if not status_ok(response.status_code): - raise AirbyteCloudError( - context={ - "workspace_id": workspace_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) - - assert response.workspaces_response is not None - page_data = response.workspaces_response.data + body = _make_public_api_request( + method="GET", + api_root=api_root, + path="/workspaces", + params={ + "offset": current_offset, + "limit": page_limit, + }, + response_model=None, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + error_context={"workspace_id": workspace_id, "api_root": api_root}, + ) + page_data = _decode_list_items( + body, item_model=models.WorkspaceResponse, list_path="/workspaces" + ) if not page_data: break @@ -692,7 +754,7 @@ def list_workspaces( if remaining is not None: remaining -= len(page_results) - if not response.workspaces_response.next: + if not (body or {}).get("next"): break current_offset += len(page_data) @@ -717,39 +779,26 @@ def list_sources( _validate_pagination_params(limit=limit) name_filter = (lambda n: n == name) if name is not None else name_filter or (lambda _: True) - _ = workspace_id # Not used (yet) - airbyte_instance: airbyte_api.AirbyteAPI = get_airbyte_server_instance( - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - api_root=api_root, - ) result: list[models.SourceResponse] = [] current_offset = 0 remaining = limit - base_context = {"workspace_id": workspace_id, "api_root": api_root} while remaining is None or remaining > 0: - try: - response: api.ListSourcesResponse = airbyte_instance.sources.list_sources( - api.ListSourcesRequest( - workspace_ids=[workspace_id], - offset=current_offset, - limit=PAGE_SIZE, - ), - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e - - if not status_ok(response.status_code): - raise AirbyteCloudError( - context={ - "workspace_id": workspace_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) - assert response.sources_response is not None - page_data = response.sources_response.data + body = _make_public_api_request( + method="GET", + api_root=api_root, + path="/sources", + params={ + "workspaceIds": [workspace_id], + "offset": current_offset, + "limit": PAGE_SIZE, + }, + response_model=None, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + error_context={"workspace_id": workspace_id, "api_root": api_root}, + ) + page_data = _decode_list_items(body, item_model=models.SourceResponse, list_path="/sources") if not page_data: break @@ -759,7 +808,7 @@ def list_sources( if remaining is not None: remaining -= len(page_results) - if not response.sources_response.next: + if not (body or {}).get("next"): break current_offset += len(page_data) @@ -784,39 +833,28 @@ def list_destinations( _validate_pagination_params(limit=limit) name_filter = (lambda n: n == name) if name is not None else name_filter or (lambda _: True) - _ = workspace_id # Not used (yet) - airbyte_instance = get_airbyte_server_instance( - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - api_root=api_root, - ) result: list[models.DestinationResponse] = [] current_offset = 0 remaining = limit - base_context = {"workspace_id": workspace_id, "api_root": api_root} while remaining is None or remaining > 0: - try: - response = airbyte_instance.destinations.list_destinations( - api.ListDestinationsRequest( - workspace_ids=[workspace_id], - offset=current_offset, - limit=PAGE_SIZE, - ), - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e - - if not status_ok(response.status_code): - raise AirbyteCloudError( - context={ - "workspace_id": workspace_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) - assert response.destinations_response is not None - page_data = response.destinations_response.data + body = _make_public_api_request( + method="GET", + api_root=api_root, + path="/destinations", + params={ + "workspaceIds": [workspace_id], + "offset": current_offset, + "limit": PAGE_SIZE, + }, + response_model=None, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + error_context={"workspace_id": workspace_id, "api_root": api_root}, + ) + page_data = _decode_list_items( + body, item_model=models.DestinationResponse, list_path="/destinations" + ) if not page_data: break @@ -830,7 +868,7 @@ def list_destinations( if remaining is not None: remaining -= len(page_results) - if not response.destinations_response.next: + if not (body or {}).get("next"): break current_offset += len(page_data) @@ -852,37 +890,29 @@ def get_connection( ) -> models.ConnectionResponse: """Get a connection.""" _ = workspace_id # Not used (yet) - airbyte_instance = get_airbyte_server_instance( - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - api_root=api_root, - ) base_context = { "workspace_id": workspace_id, "connection_id": connection_id, "api_root": api_root, } - try: - response = airbyte_instance.connections.get_connection( - api.GetConnectionRequest( - connection_id=connection_id, - ), - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e + connection = _make_public_api_request( + method="GET", + api_root=api_root, + path=f"/connections/{connection_id}", + response_model=models.ConnectionResponse, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + error_context=base_context, + ) - if status_ok(response.status_code) and response.connection_response: - return response.connection_response + if connection: + return connection raise AirbyteMissingResourceError( resource_name_or_id=connection_id, resource_type="connection", - log_text=response.raw_response.text, - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, + context=base_context, ) @@ -902,37 +932,36 @@ def run_connection( If raise_on_failure is True, this will raise an exception if the connection fails. """ _ = workspace_id # Not used (yet) - airbyte_instance = get_airbyte_server_instance( - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - api_root=api_root, - ) try: - response = airbyte_instance.jobs.create_job( - models.JobCreateRequest( + job = _make_public_api_request( + method="POST", + api_root=api_root, + path="/jobs", + request=models.JobCreateRequest( connection_id=connection_id, job_type=models.JobTypeEnum.SYNC, ), + response_model=models.JobResponse, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + error_context={"workspace_id": workspace_id, "connection_id": connection_id}, ) - except SDKError as e: + except AirbyteCloudError as e: raise AirbyteConnectionSyncError( connection_id=connection_id, - message=f"API error occurred: {e.message}", - context={"workspace_id": workspace_id, **_get_sdk_error_context(e)}, + message=e.message, + context={"workspace_id": workspace_id, **(e.context or {})}, ) from e - if status_ok(response.status_code) and response.job_response: - return response.job_response + if job: + return job raise AirbyteConnectionSyncError( connection_id=connection_id, context={ "workspace_id": workspace_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, }, - response=response, ) @@ -974,12 +1003,6 @@ def get_job_logs( # noqa: PLR0913 # Too many arguments - needed for auth flexi A list of JobResponse objects. """ _validate_pagination_params(limit=limit) - airbyte_instance = get_airbyte_server_instance( - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - api_root=api_root, - ) result: list[models.JobResponse] = [] current_offset = offset or 0 remaining = limit @@ -993,61 +1016,35 @@ def get_job_logs( # noqa: PLR0913 # Too many arguments - needed for auth flexi job_type_value = models.JobTypeEnum(job_type) except ValueError: valid_job_types = ", ".join(job_type_enum.value for job_type_enum in models.JobTypeEnum) - raise AirbyteLibInputError( - message=f"`job_type` must be one of: {valid_job_types}.", - input_value=job_type, - ) from None - else: - job_type_value = job_type - - while remaining is None or remaining > 0: - page_limit = _get_page_limit(remaining) - try: - response: api.ListJobsResponse = airbyte_instance.jobs.list_jobs( - api.ListJobsRequest( - workspace_ids=[workspace_id], - connection_id=connection_id, - limit=page_limit, - offset=current_offset, - order_by=order_by, - job_type=job_type_value, - ), - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e - - if not status_ok(response.status_code): - if response.status_code == HTTPStatus.NOT_FOUND: - raise AirbyteMissingResourceError( - response=response, - resource_type="job", - context={ - **base_context, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) - raise AirbyteCloudError( - message="Failed to list jobs.", - response=response, - context={ - **base_context, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) - if not response.jobs_response: - raise AirbyteCloudError( - message="Jobs response payload was empty.", - response=response, - context={ - **base_context, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) + raise AirbyteLibInputError( + message=f"`job_type` must be one of: {valid_job_types}.", + input_value=job_type, + ) from None + else: + job_type_value = job_type + + while remaining is None or remaining > 0: + page_limit = _get_page_limit(remaining) + body = _make_public_api_request( + method="GET", + api_root=api_root, + path="/jobs", + params={ + "workspaceIds": [workspace_id], + "connectionId": connection_id, + "limit": page_limit, + "offset": current_offset, + "orderBy": order_by, + "jobType": job_type_value, + }, + response_model=None, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + error_context=base_context, + ) - page_data: list[models.JobResponse] = list(response.jobs_response.data) + page_data = _decode_list_items(body, item_model=models.JobResponse, list_path="/jobs") if not page_data: break @@ -1056,7 +1053,7 @@ def get_job_logs( # noqa: PLR0913 # Too many arguments - needed for auth flexi remaining -= len(page_data) current_offset += len(page_data) - if not response.jobs_response.next or len(page_data) < page_limit: + if not (body or {}).get("next") or len(page_data) < page_limit: break return result if limit is None else result[:limit] @@ -1071,32 +1068,23 @@ def get_job_info( bearer_token: SecretString | None, ) -> models.JobResponse: """Get a job.""" - airbyte_instance = get_airbyte_server_instance( + job = _make_public_api_request( + method="GET", + api_root=api_root, + path=f"/jobs/{job_id}", + response_model=models.JobResponse, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, + error_context={"job_id": job_id}, ) - try: - response = airbyte_instance.jobs.get_job( - api.GetJobRequest( - job_id=job_id, - ), - ) - except SDKError as e: - raise _wrap_sdk_error(e, {"job_id": job_id}) from e - if status_ok(response.status_code) and response.job_response: - return response.job_response + if job: + return job raise AirbyteMissingResourceError( resource_name_or_id=str(job_id), resource_type="job", - log_text=response.raw_response.text, - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, ) @@ -1109,49 +1097,22 @@ def cancel_job( bearer_token: SecretString | None, ) -> models.JobResponse: """Cancel a running job.""" - airbyte_instance = get_airbyte_server_instance( + job = _make_public_api_request( + method="DELETE", + api_root=api_root, + path=f"/jobs/{job_id}", + response_model=models.JobResponse, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, + error_context={"job_id": job_id}, ) - response = airbyte_instance.jobs.cancel_job( - api.CancelJobRequest( - job_id=job_id, - ), - ) - if status_ok(response.status_code): - if response.job_response: - return response.job_response - raise AirbyteCloudError( - message="Job cancellation response payload was empty.", - response=response, - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) - - if response.status_code == HTTPStatus.NOT_FOUND: - raise AirbyteMissingResourceError( - resource_name_or_id=str(job_id), - resource_type="job", - response=response, - log_text=response.raw_response.text, - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) + if job: + return job raise AirbyteCloudError( - message="Could not cancel job.", - response=response, - log_text=response.raw_response.text, - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, + message="Job cancellation response payload was empty.", + context={"job_id": job_id}, ) @@ -1173,31 +1134,27 @@ def create_source( Either `definition_id` or `config[sourceType]` must be provided. """ - airbyte_instance = get_airbyte_server_instance( - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, + source = _make_public_api_request( + method="POST", api_root=api_root, - ) - response: api.CreateSourceResponse = airbyte_instance.sources.create_source( - models.SourceCreateRequest( + path="/sources", + request=models.SourceCreateRequest( name=name, workspace_id=workspace_id, - configuration=config, # Speakeasy API wants a dataclass, not a dict + configuration=config, definition_id=definition_id or None, # Only used for custom sources secret_id=None, # For OAuth, not yet supported ), + response_model=models.SourceResponse, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, ) - if status_ok(response.status_code) and response.source_response: - return response.source_response + if source: + return source raise AirbyteCloudError( message="Could not create source.", - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - response=response, ) @@ -1213,37 +1170,27 @@ def get_source( Secrets in the returned configuration are redacted by the API. """ - airbyte_instance = get_airbyte_server_instance( + base_context = {"source_id": source_id, "api_root": api_root} + body = _make_public_api_request( + method="GET", + api_root=api_root, + path=f"/sources/{source_id}", + response_model=None, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, + error_context=base_context, ) - base_context = {"source_id": source_id, "api_root": api_root} - try: - response = airbyte_instance.sources.get_source( - api.GetSourceRequest( - source_id=source_id, - ), - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e - - if status_ok(response.status_code) and response.source_response: - raw_response: dict[str, Any] = json.loads(response.raw_response.text) - source = response.source_response - config = raw_response.get("configuration") or {} - source.configuration = config # pyrefly: ignore[bad-assignment] # Raw config. + + if body: + source = models.SourceResponse.model_validate(body) + source.configuration = body.get("configuration") or {} # pyrefly: ignore[bad-assignment] return source raise AirbyteMissingResourceError( resource_name_or_id=source_id, resource_type="source", - log_text=response.raw_response.text, - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, + context=base_context, ) @@ -1304,25 +1251,16 @@ def delete_source( }, ) - airbyte_instance = get_airbyte_server_instance( + _make_public_api_request( + method="DELETE", + api_root=api_root, + path=f"/sources/{source_id}", + response_model=None, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, - ) - response = airbyte_instance.sources.delete_source( - api.DeleteSourceRequest( - source_id=source_id, - ), + error_context={"source_id": source_id}, ) - if not status_ok(response.status_code): - raise AirbyteCloudError( - context={ - "source_id": source_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) def patch_source( @@ -1352,32 +1290,28 @@ def patch_source( Returns: Updated SourceResponse object """ - airbyte_instance = get_airbyte_server_instance( + source = _make_public_api_request( + method="PATCH", + api_root=api_root, + path=f"/sources/{source_id}", + request=models.SourcePatchRequest( + name=name, + configuration=config, + ), + response_model=models.SourceResponse, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, - ) - response = airbyte_instance.sources.patch_source( - api.PatchSourceRequest( - source_id=source_id, - source_patch_request=models.SourcePatchRequest( - name=name, - configuration=config, - ), - ), + error_context={"source_id": source_id}, ) - if status_ok(response.status_code) and response.source_response: - return response.source_response + if source: + return source raise AirbyteCloudError( message="Could not update source.", context={ "source_id": source_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, }, - response=response, ) @@ -1385,12 +1319,9 @@ def patch_source( def _get_destination_type_str( - destination: DestinationConfiguration | dict[str, Any], + destination: dict[str, Any], ) -> str: - if isinstance(destination, dict): - destination_type = destination.get("destinationType") - else: - destination_type = getattr(destination, "DESTINATION_TYPE", None) + destination_type = destination.get("destinationType") if not destination_type or not isinstance(destination_type, str): raise AirbyteLibInputError( @@ -1410,42 +1341,38 @@ def create_destination( name: str, *, workspace_id: str, - config: DestinationConfiguration | dict[str, Any], + config: dict[str, Any], api_root: str, client_id: SecretString | None, client_secret: SecretString | None, bearer_token: SecretString | None, ) -> models.DestinationResponse: """Get a connection.""" - airbyte_instance = get_airbyte_server_instance( - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - api_root=api_root, - ) definition_id_override: str | None = None if _get_destination_type_str(config) == "dev-null": # TODO: We have to hard-code the definition ID for dev-null destination. # https://github.com/airbytehq/PyAirbyte/issues/743 definition_id_override = "a7bcc9d8-13b3-4e49-b80d-d020b90045e3" - response: api.CreateDestinationResponse = airbyte_instance.destinations.create_destination( - models.DestinationCreateRequest( + destination = _make_public_api_request( + method="POST", + api_root=api_root, + path="/destinations", + request=models.DestinationCreateRequest( definition_id=definition_id_override, name=name, workspace_id=workspace_id, - configuration=config, # Speakeasy API wants a dataclass, not a dict + configuration=config, ), + response_model=models.DestinationResponse, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, ) - if status_ok(response.status_code) and response.destination_response: - return response.destination_response + if destination: + return destination raise AirbyteCloudError( message="Could not create destination.", - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - response=response, ) @@ -1461,37 +1388,29 @@ def get_destination( Secrets in the returned configuration are redacted by the API. """ - airbyte_instance = get_airbyte_server_instance( + base_context = {"destination_id": destination_id, "api_root": api_root} + body = _make_public_api_request( + method="GET", + api_root=api_root, + path=f"/destinations/{destination_id}", + response_model=None, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, + error_context=base_context, ) - base_context = {"destination_id": destination_id, "api_root": api_root} - try: - response = airbyte_instance.destinations.get_destination( - api.GetDestinationRequest( - destination_id=destination_id, - ), - ) - except SDKError as e: - raise _wrap_sdk_error(e, base_context) from e - if status_ok(response.status_code) and response.destination_response: - raw_response: dict[str, Any] = json.loads(response.raw_response.text) - response.destination_response.configuration = ( - raw_response.get("configuration") or {} # pyrefly: ignore[bad-assignment] + if body: + destination = models.DestinationResponse.model_validate(body) + destination.configuration = ( + body.get("configuration") or {} # pyrefly: ignore[bad-assignment] ) - return response.destination_response + return destination raise AirbyteMissingResourceError( resource_name_or_id=destination_id, resource_type="destination", - log_text=response.raw_response.text, - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, + context=base_context, ) @@ -1573,29 +1492,22 @@ def get_source_definition( bearer_token: SecretString | None, ) -> models.DefinitionResponse: """Get a source connector definition, including its `docker_repository` name.""" - airbyte_instance = get_airbyte_server_instance( + definition = _make_public_api_request( + method="GET", + api_root=api_root, + path=f"/workspaces/{workspace_id}/definitions/sources/{definition_id}", + response_model=models.DefinitionResponse, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, - ) - response = airbyte_instance.source_definitions.get_source_definition( - api.GetSourceDefinitionRequest( - definition_id=definition_id, - workspace_id=workspace_id, - ), + error_context={"definition_id": definition_id, "workspace_id": workspace_id}, ) - if status_ok(response.status_code) and response.definition_response: - return response.definition_response + if definition: + return definition raise AirbyteMissingResourceError( resource_name_or_id=definition_id, resource_type="source definition", - log_text=response.raw_response.text, - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, ) @@ -1609,29 +1521,22 @@ def get_destination_definition( bearer_token: SecretString | None, ) -> models.DefinitionResponse: """Get a destination connector definition, including its `docker_repository` name.""" - airbyte_instance = get_airbyte_server_instance( + definition = _make_public_api_request( + method="GET", + api_root=api_root, + path=f"/workspaces/{workspace_id}/definitions/destinations/{definition_id}", + response_model=models.DefinitionResponse, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, - ) - response = airbyte_instance.destination_definitions.get_destination_definition( - api.GetDestinationDefinitionRequest( - definition_id=definition_id, - workspace_id=workspace_id, - ), + error_context={"definition_id": definition_id, "workspace_id": workspace_id}, ) - if status_ok(response.status_code) and response.definition_response: - return response.definition_response + if definition: + return definition raise AirbyteMissingResourceError( resource_name_or_id=definition_id, resource_type="destination definition", - log_text=response.raw_response.text, - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, ) @@ -1693,25 +1598,16 @@ def delete_destination( }, ) - airbyte_instance = get_airbyte_server_instance( + _make_public_api_request( + method="DELETE", + api_root=api_root, + path=f"/destinations/{destination_id}", + response_model=None, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, + error_context={"destination_id": destination_id}, ) - response = airbyte_instance.destinations.delete_destination( - api.DeleteDestinationRequest( - destination_id=destination_id, - ), - ) - if not status_ok(response.status_code): - raise AirbyteCloudError( - context={ - "destination_id": destination_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) def patch_destination( @@ -1722,7 +1618,7 @@ def patch_destination( client_secret: SecretString | None, bearer_token: SecretString | None, name: str | None = None, - config: DestinationConfiguration | dict[str, Any] | None = None, + config: dict[str, Any] | None = None, ) -> models.DestinationResponse: """Update/patch a destination configuration. @@ -1741,32 +1637,28 @@ def patch_destination( Returns: Updated DestinationResponse object """ - airbyte_instance = get_airbyte_server_instance( + destination = _make_public_api_request( + method="PATCH", + api_root=api_root, + path=f"/destinations/{destination_id}", + request=models.DestinationPatchRequest( + name=name, + configuration=config, + ), + response_model=models.DestinationResponse, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, - ) - response = airbyte_instance.destinations.patch_destination( - api.PatchDestinationRequest( - destination_id=destination_id, - destination_patch_request=models.DestinationPatchRequest( - name=name, - configuration=config, - ), - ), + error_context={"destination_id": destination_id}, ) - if status_ok(response.status_code) and response.destination_response: - return response.destination_response + if destination: + return destination raise AirbyteCloudError( message="Could not update destination.", context={ "destination_id": destination_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, }, - response=response, ) @@ -1796,9 +1688,9 @@ def build_stream_configurations( def build_connection_schedule( schedule_type: str, cron_expression: str | None = None, -) -> models.AirbyteAPIConnectionSchedule: +) -> models.AirbyteApiConnectionSchedule: """Build a connection schedule object.""" - return models.AirbyteAPIConnectionSchedule( + return models.AirbyteApiConnectionSchedule( schedule_type=models.ScheduleTypeEnum(schedule_type), cron_expression=cron_expression, ) @@ -1818,33 +1710,33 @@ def create_connection( # noqa: PLR0913 # Too many arguments selected_stream_names: list[str], ) -> models.ConnectionResponse: _ = workspace_id # Not used (yet) - airbyte_instance = get_airbyte_server_instance( - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - api_root=api_root, - ) stream_configurations_obj = build_stream_configurations(selected_stream_names) - response = airbyte_instance.connections.create_connection( - models.ConnectionCreateRequest( + connection = _make_public_api_request( + method="POST", + api_root=api_root, + path="/connections", + request=models.ConnectionCreateRequest( name=name, source_id=source_id, destination_id=destination_id, configurations=stream_configurations_obj, prefix=prefix, ), + response_model=models.ConnectionResponse, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + error_context={"source_id": source_id, "destination_id": destination_id}, ) - if not status_ok(response.status_code) or response.connection_response is None: + if connection is None: raise AirbyteCloudError( context={ "source_id": source_id, "destination_id": destination_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, }, ) - return response.connection_response + return connection def get_connection_by_name( @@ -1957,25 +1849,16 @@ def delete_connection( ) _ = workspace_id # Not used (yet) - airbyte_instance = get_airbyte_server_instance( + _make_public_api_request( + method="DELETE", + api_root=api_root, + path=f"/connections/{connection_id}", + response_model=None, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, - api_root=api_root, - ) - response = airbyte_instance.connections.delete_connection( - api.DeleteConnectionRequest( - connection_id=connection_id, - ), + error_context={"connection_id": connection_id}, ) - if not status_ok(response.status_code): - raise AirbyteCloudError( - context={ - "connection_id": connection_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, - ) def patch_connection( # noqa: PLR0913 # Too many arguments @@ -1986,8 +1869,8 @@ def patch_connection( # noqa: PLR0913 # Too many arguments client_secret: SecretString | None, bearer_token: SecretString | None, name: str | None = None, - configurations: models.StreamConfigurationsInput | None = None, - schedule: models.AirbyteAPIConnectionSchedule | None = None, + configurations: models.StreamConfigurations | None = None, + schedule: models.AirbyteApiConnectionSchedule | None = None, prefix: str | None = None, status: str | models.ConnectionStatusEnum | None = None, ) -> models.ConnectionResponse: @@ -2011,53 +1894,45 @@ def patch_connection( # noqa: PLR0913 # Too many arguments Returns: Updated ConnectionResponse object """ - airbyte_instance = get_airbyte_server_instance( - client_id=client_id, - client_secret=client_secret, - bearer_token=bearer_token, - api_root=api_root, - ) if isinstance(status, str): - try: - status_value = models.ConnectionStatusEnum(status) - except ValueError: - valid_statuses = ", ".join( - connection_status.value for connection_status in models.ConnectionStatusEnum - ) + valid_statuses = ", ".join( + connection_status.value for connection_status in models.ConnectionStatusEnum + ) + if status not in {m.value for m in models.ConnectionStatusEnum}: raise AirbyteLibInputError( message=f"`status` must be one of: {valid_statuses}.", input_value=status, - ) from None + ) + status_value = models.ConnectionStatusEnum(status) else: status_value = status - try: - response = airbyte_instance.connections.patch_connection( - api.PatchConnectionRequest( - connection_id=connection_id, - connection_patch_request=models.ConnectionPatchRequest( - name=name, - configurations=configurations, - schedule=schedule, - prefix=prefix, - status=status_value, - ), - ), - ) - except SDKError as e: - raise _wrap_sdk_error(e, {"connection_id": connection_id}) from e + connection = _make_public_api_request( + method="PATCH", + api_root=api_root, + path=f"/connections/{connection_id}", + request=models.ConnectionPatchRequest( + name=name, + configurations=configurations, + schedule=schedule, + prefix=prefix, + status=status_value, + ), + response_model=models.ConnectionResponse, + client_id=client_id, + client_secret=client_secret, + bearer_token=bearer_token, + error_context={"connection_id": connection_id}, + ) - if status_ok(response.status_code) and response.connection_response: - return response.connection_response + if connection: + return connection raise AirbyteCloudError( message="Could not update connection.", context={ "connection_id": connection_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, }, - response=response, ) @@ -2095,7 +1970,37 @@ def get_bearer_token( return SecretString(response.json()["access_token"]) -def _make_config_api_request( +@overload +def _make_config_api_request( # Mirrors the API surface. + *, + api_root: str, + path: str, + request: BaseModel | dict[str, Any], + response_model: type[_T], + client_id: SecretString | None, + client_secret: SecretString | None, + bearer_token: SecretString | None, + config_api_root: str | None = None, + timeout: tuple[float, float] | None = None, +) -> _T: ... + + +@overload +def _make_config_api_request( # Mirrors the API surface. + *, + api_root: str, + path: str, + request: BaseModel | dict[str, Any], + response_model: None = None, + client_id: SecretString | None, + client_secret: SecretString | None, + bearer_token: SecretString | None, + config_api_root: str | None = None, + timeout: tuple[float, float] | None = None, +) -> dict[str, Any]: ... + + +def _make_config_api_request( # noqa: PLR0913 # Mirrors the API surface. *, api_root: str, path: str, @@ -2411,30 +2316,26 @@ def create_custom_yaml_source_definition( bearer_token: SecretString | None, ) -> models.DeclarativeSourceDefinitionResponse: """Create a custom YAML source definition.""" - airbyte_instance = get_airbyte_server_instance( + definition = _make_public_api_request( + method="POST", api_root=api_root, + path=f"/workspaces/{workspace_id}/definitions/declarative_sources", + request=models.CreateDeclarativeSourceDefinitionRequest( + name=name, + manifest=manifest, + ), + response_model=models.DeclarativeSourceDefinitionResponse, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, + error_context={"name": name, "workspace_id": workspace_id}, ) - - request_body = models.CreateDeclarativeSourceDefinitionRequest( - name=name, - manifest=manifest, - ) - request = api.CreateDeclarativeSourceDefinitionRequest( - workspace_id=workspace_id, - create_declarative_source_definition_request=request_body, - ) - response = airbyte_instance.declarative_source_definitions.create_declarative_source_definition( - request - ) - if response.declarative_source_definition_response is None: + if definition is None: raise AirbyteCloudError( message="Failed to create custom YAML source definition", context={"name": name, "workspace_id": workspace_id}, ) - return response.declarative_source_definition_response + return definition def list_custom_yaml_source_definitions( @@ -2446,32 +2347,27 @@ def list_custom_yaml_source_definitions( bearer_token: SecretString | None, ) -> list[models.DeclarativeSourceDefinitionResponse]: """List all custom YAML source definitions in a workspace.""" - airbyte_instance = get_airbyte_server_instance( + path = f"/workspaces/{workspace_id}/definitions/declarative_sources" + body = _make_public_api_request( + method="GET", api_root=api_root, + path=path, + response_model=None, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, + error_context={"workspace_id": workspace_id}, ) - - request = api.ListDeclarativeSourceDefinitionsRequest( - workspace_id=workspace_id, - ) - response = airbyte_instance.declarative_source_definitions.list_declarative_source_definitions( - request - ) - if ( - not status_ok(response.status_code) - or response.declarative_source_definitions_response is None - ): + if body is None: raise AirbyteCloudError( message="Failed to list custom YAML source definitions", context={ "workspace_id": workspace_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, }, ) - return response.declarative_source_definitions_response.data + return _decode_list_items( + body, item_model=models.DeclarativeSourceDefinitionResponse, list_path=path + ) def get_custom_yaml_source_definition( @@ -2484,34 +2380,28 @@ def get_custom_yaml_source_definition( bearer_token: SecretString | None, ) -> models.DeclarativeSourceDefinitionResponse: """Get a specific custom YAML source definition.""" - airbyte_instance = get_airbyte_server_instance( + definition = _make_public_api_request( + method="GET", api_root=api_root, + path=f"/workspaces/{workspace_id}/definitions/declarative_sources/{definition_id}", + response_model=models.DeclarativeSourceDefinitionResponse, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, + error_context={ + "workspace_id": workspace_id, + "definition_id": definition_id, + }, ) - - request = api.GetDeclarativeSourceDefinitionRequest( - workspace_id=workspace_id, - definition_id=definition_id, - ) - response = airbyte_instance.declarative_source_definitions.get_declarative_source_definition( - request - ) - if ( - not status_ok(response.status_code) - or response.declarative_source_definition_response is None - ): + if definition is None: raise AirbyteCloudError( message="Failed to get custom YAML source definition", context={ "workspace_id": workspace_id, "definition_id": definition_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, }, ) - return response.declarative_source_definition_response + return definition def update_custom_yaml_source_definition( @@ -2525,38 +2415,31 @@ def update_custom_yaml_source_definition( bearer_token: SecretString | None, ) -> models.DeclarativeSourceDefinitionResponse: """Update a custom YAML source definition.""" - airbyte_instance = get_airbyte_server_instance( + definition = _make_public_api_request( + method="PUT", api_root=api_root, + path=f"/workspaces/{workspace_id}/definitions/declarative_sources/{definition_id}", + request=models.UpdateDeclarativeSourceDefinitionRequest( + manifest=manifest, + ), + response_model=models.DeclarativeSourceDefinitionResponse, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, + error_context={ + "workspace_id": workspace_id, + "definition_id": definition_id, + }, ) - - request_body = models.UpdateDeclarativeSourceDefinitionRequest( - manifest=manifest, - ) - request = api.UpdateDeclarativeSourceDefinitionRequest( - workspace_id=workspace_id, - definition_id=definition_id, - update_declarative_source_definition_request=request_body, - ) - response = airbyte_instance.declarative_source_definitions.update_declarative_source_definition( - request - ) - if ( - not status_ok(response.status_code) - or response.declarative_source_definition_response is None - ): + if definition is None: raise AirbyteCloudError( message="Failed to update custom YAML source definition", context={ "workspace_id": workspace_id, "definition_id": definition_id, - "request_url": response.raw_response.url, - "status_code": response.status_code, }, ) - return response.declarative_source_definition_response + return definition def delete_custom_yaml_source_definition( @@ -2615,29 +2498,20 @@ def delete_custom_yaml_source_definition( # Else proceed with deletion - airbyte_instance = get_airbyte_server_instance( + _make_public_api_request( + method="DELETE", api_root=api_root, + path=f"/workspaces/{workspace_id}/definitions/declarative_sources/{definition_id}", + response_model=None, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, + error_context={ + "workspace_id": workspace_id, + "definition_id": definition_id, + }, ) - request = api.DeleteDeclarativeSourceDefinitionRequest( - workspace_id=workspace_id, - definition_id=definition_id, - ) - response = airbyte_instance.declarative_source_definitions.delete_declarative_source_definition( - request - ) - if not status_ok(response.status_code): - raise AirbyteCloudError( - context={ - "workspace_id": workspace_id, - "definition_id": definition_id, - "response": response, - }, - ) - def get_connector_builder_project_for_definition_id( *, @@ -2871,23 +2745,23 @@ def list_organizations_for_user( Returns: List of OrganizationResponse objects containing organization_id, organization_name, email """ - airbyte_instance = get_airbyte_server_instance( + body = _make_public_api_request( + method="GET", api_root=api_root, + path="/organizations", + response_model=None, client_id=client_id, client_secret=client_secret, bearer_token=bearer_token, ) - response = airbyte_instance.organizations.list_organizations_for_user() - if status_ok(response.status_code) and response.organizations_response: - return response.organizations_response.data + if body is not None: + return _decode_list_items( + body, item_model=models.OrganizationResponse, list_path="/organizations" + ) raise AirbyteCloudError( message="Failed to list organizations for user.", - context={ - "request_url": response.raw_response.url, - "status_code": response.status_code, - }, ) diff --git a/airbyte/caches/_utils/_cache_to_dest.py b/airbyte/caches/_utils/_cache_to_dest.py index 518d75f62..27b944c45 100644 --- a/airbyte/caches/_utils/_cache_to_dest.py +++ b/airbyte/caches/_utils/_cache_to_dest.py @@ -6,16 +6,6 @@ from pathlib import Path from typing import TYPE_CHECKING, Any -from airbyte_api.models import ( - BatchedStandardInserts, - DatasetLocation, - DestinationBigquery, - DestinationDuckdb, - DestinationPostgres, - DestinationSnowflake, - UsernameAndPassword, -) - from airbyte.exceptions import AirbyteLibInputError from airbyte.secrets.base import SecretString @@ -23,7 +13,6 @@ if TYPE_CHECKING: from collections.abc import Callable - from airbyte._util import api_util from airbyte.caches.base import CacheBase from airbyte.caches.bigquery import BigQueryCache from airbyte.caches.duckdb import DuckDBCache @@ -37,9 +26,9 @@ def cache_to_destination_configuration( cache: CacheBase, -) -> api_util.DestinationConfiguration: +) -> dict[str, Any]: """Get the destination configuration from the cache.""" - conversion_fn_map: dict[str, Callable[[Any], api_util.DestinationConfiguration]] = { + conversion_fn_map: dict[str, Callable[[Any], dict[str, Any]]] = { "BigQueryCache": bigquery_cache_to_destination_configuration, "bigquery": bigquery_cache_to_destination_configuration, "DuckDBCache": duckdb_cache_to_destination_configuration, @@ -67,59 +56,71 @@ def cache_to_destination_configuration( def duckdb_cache_to_destination_configuration( cache: DuckDBCache, -) -> DestinationDuckdb: +) -> dict[str, Any]: """Get the destination configuration from the DuckDB cache.""" - return DestinationDuckdb( - destination_path=str(cache.db_path), - schema=cache.schema_name, - ) + return { + "destination_path": str(cache.db_path), + "destinationType": "duckdb", + "schema": cache.schema_name, + } def motherduck_cache_to_destination_configuration( cache: MotherDuckCache, -) -> DestinationDuckdb: +) -> dict[str, Any]: """Get the destination configuration from the DuckDB cache.""" - return DestinationDuckdb( - destination_path=cache.db_path, - schema=cache.schema_name, - motherduck_api_key=cache.api_key, - ) + return { + "destination_path": cache.db_path, + "destinationType": "duckdb", + "motherduck_api_key": cache.api_key, + "schema": cache.schema_name, + } def postgres_cache_to_destination_configuration( cache: PostgresCache, -) -> DestinationPostgres: +) -> dict[str, Any]: """Get the destination configuration from the Postgres cache.""" - return DestinationPostgres( - database=cache.database, - host=cache.host, - password=cache.password, - port=cache.port, - schema=cache.schema_name, - username=cache.username, - ) + return { + "database": cache.database, + "host": cache.host, + "username": cache.username, + "destinationType": "postgres", + "disable_type_dedupe": False, + "drop_cascade": False, + "password": cache.password, + "port": cache.port, + "schema": cache.schema_name, + "ssl": False, + "unconstrained_number": False, + } def snowflake_cache_to_destination_configuration( cache: SnowflakeCache, -) -> DestinationSnowflake: +) -> dict[str, Any]: """Get the destination configuration from the Snowflake cache.""" - return DestinationSnowflake( - host=f"{cache.account}.snowflakecomputing.com", - database=cache.get_database_name().upper(), - schema=cache.schema_name.upper(), - warehouse=cache.warehouse, - role=cache.role, - username=cache.username, - credentials=UsernameAndPassword( - password=cache.password, # pyrefly: ignore[bad-argument-type] - ), - ) + return { + "host": f"{cache.account}.snowflakecomputing.com", + "database": cache.get_database_name().upper(), + "schema": cache.schema_name.upper(), + "warehouse": cache.warehouse, + "role": cache.role, + "username": cache.username, + "credentials": { + "password": cache.password, + "auth_type": "Username and Password", + }, + "destinationType": "snowflake", + "disable_type_dedupe": False, + "retention_period_days": 1, + "use_merge_for_upsert": False, + } def bigquery_cache_to_destination_configuration( cache: BigQueryCache, -) -> DestinationBigquery: +) -> dict[str, Any]: """Get the destination configuration from the BigQuery cache.""" credentials_json: str | None = ( SecretString(Path(cache.credentials_path).read_text(encoding="utf-8")) @@ -127,10 +128,13 @@ def bigquery_cache_to_destination_configuration( else None ) - return DestinationBigquery( - project_id=cache.project_name, - dataset_id=cache.dataset_name, - dataset_location=DatasetLocation(cache.dataset_location), - credentials_json=credentials_json, - loading_method=BatchedStandardInserts(), - ) + return { + "project_id": cache.project_name, + "dataset_id": cache.dataset_name, + "dataset_location": cache.dataset_location, + "cdc_deletion_mode": "Hard delete", + "credentials_json": credentials_json, + "destinationType": "bigquery", + "disable_type_dedupe": False, + "loading_method": {"method": "Standard"}, + } diff --git a/airbyte/caches/_utils/_dest_to_cache.py b/airbyte/caches/_utils/_dest_to_cache.py index ecc0a15a9..a9c408c15 100644 --- a/airbyte/caches/_utils/_dest_to_cache.py +++ b/airbyte/caches/_utils/_dest_to_cache.py @@ -8,12 +8,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any -from airbyte_api.models import ( - DestinationBigquery, - DestinationDuckdb, - DestinationPostgres, - DestinationSnowflake, -) +from pydantic import BaseModel from airbyte.caches.base import CacheBase from airbyte.caches.bigquery import BigQueryCache @@ -29,7 +24,6 @@ if TYPE_CHECKING: from collections.abc import Callable - from airbyte._util import api_util from airbyte.caches.base import CacheBase @@ -50,40 +44,60 @@ def get_supported_destination_types() -> set[str]: return _SUPPORTED_DESTINATION_TYPES +def _required_key( + destination_configuration: dict[str, Any], + key: str, +) -> Any: # noqa: ANN401 + """Read a required key from a destination configuration dict.""" + value = destination_configuration.get(key) + if value is None: + raise AirbyteLibInputError( + message=f"Destination configuration is missing required key '{key}'.", + context={"destination_configuration": destination_configuration}, + ) + return value + + def destination_to_cache( - destination_configuration: api_util.DestinationConfiguration | dict[str, Any], + destination_configuration: dict[str, Any] | BaseModel, *, schema_name: str | None = None, ) -> CacheBase: """Get the destination configuration from the cache.""" - conversion_fn_map: dict[str, Callable[[Any], CacheBase]] = { + conversion_fn_map: dict[str, Callable[[dict[str, Any]], CacheBase]] = { "bigquery": bigquery_destination_to_cache, "duckdb": duckdb_destination_to_cache, "motherduck": motherduck_destination_to_cache, "postgres": postgres_destination_to_cache, "snowflake": snowflake_destination_to_cache, } + if isinstance(destination_configuration, BaseModel): + destination_configuration = destination_configuration.model_dump(mode="json", by_alias=True) + if isinstance(destination_configuration, dict): - try: - destination_type = ( - destination_configuration.get("DESTINATION_TYPE") - or destination_configuration["destinationType"] - ) - if hasattr(destination_type, "value"): - destination_type = destination_type.value - elif hasattr(destination_type, "_value_"): - destination_type = destination_type._value_ - else: - destination_type = str(destination_type) - except KeyError as ex: + destination_type = destination_configuration.get( + "DESTINATION_TYPE" + ) or destination_configuration.get("destinationType") + if destination_type is None: raise AirbyteLibInputError( message=( "Missing 'destinationType' in keys " f"{list(destination_configuration.keys())}." ), - ) from ex + ) from None + if hasattr(destination_type, "value"): + destination_type = destination_type.value + elif hasattr(destination_type, "_value_"): + destination_type = destination_type._value_ + else: + destination_type = str(destination_type) else: - destination_type = destination_configuration.DESTINATION_TYPE.value + raise AirbyteLibInputError( + message=( + "Destination configuration must be a dict or a model, " + f"got {type(destination_configuration).__name__}." + ), + ) if destination_type not in conversion_fn_map: raise AirbyteLibInputError( @@ -106,7 +120,7 @@ def destination_to_cache( def bigquery_destination_to_cache( - destination_configuration: DestinationBigquery | dict[str, Any], + destination_configuration: dict[str, Any], ) -> BigQueryCache: """Create a new BigQuery cache from the destination configuration. @@ -118,19 +132,7 @@ def bigquery_destination_to_cache( temporary file and used directly. Otherwise we fall back to the `BIGQUERY_CREDENTIALS_PATH` secret/env-var (the cloud API path). """ - # Extract credentials_json before converting to the Pydantic model, - # because DestinationBigquery may strip or obfuscate the field. - raw_credentials_json: str | None = None - if isinstance(destination_configuration, dict): - raw_credentials_json = destination_configuration.get("credentials_json") - filtered = { - k: v - for k, v in destination_configuration.items() - if k not in {"destinationType", "DESTINATION_TYPE"} - } - destination_configuration = DestinationBigquery(**filtered) - elif hasattr(destination_configuration, "credentials_json"): - raw_credentials_json = destination_configuration.credentials_json + raw_credentials_json: str | None = destination_configuration.get("credentials_json") if raw_credentials_json and "****" not in raw_credentials_json: # Plaintext credentials from a local destination config — write to @@ -146,26 +148,18 @@ def bigquery_destination_to_cache( credentials_path = get_secret("BIGQUERY_CREDENTIALS_PATH") return BigQueryCache( - project_name=destination_configuration.project_id, - dataset_name=destination_configuration.dataset_id, + project_name=_required_key(destination_configuration, "project_id"), + dataset_name=_required_key(destination_configuration, "dataset_id"), credentials_path=credentials_path, - dataset_location=destination_configuration.dataset_location, + dataset_location=destination_configuration.get("dataset_location"), ) def duckdb_destination_to_cache( - destination_configuration: DestinationDuckdb | dict[str, Any], + destination_configuration: dict[str, Any], ) -> DuckDBCache: """Create a new DuckDB cache from the destination configuration.""" - if isinstance(destination_configuration, dict): - filtered = { - k: v - for k, v in destination_configuration.items() - if k not in {"destinationType", "DESTINATION_TYPE"} - } - destination_configuration = DestinationDuckdb(**filtered) - - db_path = destination_configuration.destination_path + db_path = _required_key(destination_configuration, "destination_path") # The DuckDB destination Docker container mounts a host directory to # `/local` inside the container. Paths written as `/local/foo.duckdb` @@ -179,61 +173,44 @@ def duckdb_destination_to_cache( return DuckDBCache( db_path=db_path, - schema_name=destination_configuration.schema or "main", + schema_name=destination_configuration.get("schema") or "main", ) def motherduck_destination_to_cache( - destination_configuration: DestinationDuckdb | dict[str, Any], + destination_configuration: dict[str, Any], ) -> MotherDuckCache: """Create a new MotherDuck cache from the destination configuration.""" - if isinstance(destination_configuration, dict): - filtered = { - k: v - for k, v in destination_configuration.items() - if k not in {"destinationType", "DESTINATION_TYPE"} - } - destination_configuration = DestinationDuckdb(**filtered) - - if not destination_configuration.motherduck_api_key: + if not destination_configuration.get("motherduck_api_key"): raise AirbyteLibInputError(message="MotherDuck API key is required for MotherDuck cache.") return MotherDuckCache( - database=destination_configuration.destination_path, - schema_name=destination_configuration.schema or "main", - api_key=SecretString(destination_configuration.motherduck_api_key), + database=_required_key(destination_configuration, "destination_path"), + schema_name=destination_configuration.get("schema") or "main", + api_key=SecretString(destination_configuration["motherduck_api_key"]), ) def postgres_destination_to_cache( - destination_configuration: DestinationPostgres | dict[str, Any], + destination_configuration: dict[str, Any], ) -> PostgresCache: """Create a new Postgres cache from the destination configuration.""" - if isinstance(destination_configuration, dict): - # Strip dispatch keys before constructing the model object. - filtered = { - k: v - for k, v in destination_configuration.items() - if k not in {"destinationType", "DESTINATION_TYPE"} - } - destination_configuration = DestinationPostgres(**filtered) - - port: int = int(destination_configuration.port) if destination_configuration.port else 5432 - if not destination_configuration.password: + port: int = int(destination_configuration.get("port") or 5432) + if not destination_configuration.get("password"): raise AirbyteLibInputError(message="Password is required for Postgres cache.") return PostgresCache( - database=destination_configuration.database, - host=destination_configuration.host, - password=destination_configuration.password, + database=_required_key(destination_configuration, "database"), + host=_required_key(destination_configuration, "host"), + password=destination_configuration["password"], port=port, - schema_name=destination_configuration.schema or "public", - username=destination_configuration.username, + schema_name=destination_configuration.get("schema") or "public", + username=_required_key(destination_configuration, "username"), ) def snowflake_destination_to_cache( - destination_configuration: DestinationSnowflake | dict[str, Any], + destination_configuration: dict[str, Any], password_secret_name: str = SNOWFLAKE_PASSWORD_SECRET_NAME, ) -> SnowflakeCache: """Create a new Snowflake cache from the destination configuration. @@ -241,21 +218,10 @@ def snowflake_destination_to_cache( We may have to inject credentials, because they are obfuscated when config is returned from the REST API. """ - if isinstance(destination_configuration, dict): - filtered = { - k: v - for k, v in destination_configuration.items() - if k not in {"destinationType", "DESTINATION_TYPE"} - } - destination_configuration = DestinationSnowflake(**filtered) - snowflake_password: str | None = None - if ( - destination_configuration.credentials - and hasattr(destination_configuration.credentials, "password") - and isinstance(destination_configuration.credentials.password, str) - ): - destination_password = str(destination_configuration.credentials.password) + credentials = destination_configuration.get("credentials") or {} + if isinstance(credentials, dict) and isinstance(credentials.get("password"), str): + destination_password = str(credentials["password"]) if "****" in destination_password: try: snowflake_password = get_secret(password_secret_name) @@ -272,11 +238,11 @@ def snowflake_destination_to_cache( snowflake_password = get_secret(password_secret_name) return SnowflakeCache( - account=destination_configuration.host.split(".snowflakecomputing")[0], - database=destination_configuration.database, - schema_name=destination_configuration.schema, - warehouse=destination_configuration.warehouse, - role=destination_configuration.role, - username=destination_configuration.username, + account=_required_key(destination_configuration, "host").split(".snowflakecomputing")[0], + database=_required_key(destination_configuration, "database"), + schema_name=_required_key(destination_configuration, "schema"), + warehouse=_required_key(destination_configuration, "warehouse"), + role=_required_key(destination_configuration, "role"), + username=_required_key(destination_configuration, "username"), password=snowflake_password, ) diff --git a/airbyte/caches/bigquery.py b/airbyte/caches/bigquery.py index fe5596f9f..b8dc859b7 100644 --- a/airbyte/caches/bigquery.py +++ b/airbyte/caches/bigquery.py @@ -17,9 +17,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, NoReturn - -from airbyte_api.models import DestinationBigquery +from typing import TYPE_CHECKING, Any, ClassVar, NoReturn from airbyte._processors.sql.bigquery import BigQueryConfig, BigQuerySqlProcessor from airbyte.caches._utils._cache_to_dest import ( @@ -41,10 +39,9 @@ class BigQueryCache(BigQueryConfig, CacheBase): _sql_processor_class: ClassVar[type[SqlProcessorBase]] = BigQuerySqlProcessor paired_destination_name: ClassVar[str | None] = "destination-bigquery" - paired_destination_config_class: ClassVar[type | None] = DestinationBigquery @property - def paired_destination_config(self) -> DestinationBigquery: + def paired_destination_config(self) -> dict[str, Any]: """Return a dictionary of destination configuration values.""" return bigquery_cache_to_destination_configuration(cache=self) diff --git a/airbyte/caches/duckdb.py b/airbyte/caches/duckdb.py index c27a661fa..a8d1fe2e4 100644 --- a/airbyte/caches/duckdb.py +++ b/airbyte/caches/duckdb.py @@ -17,9 +17,8 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar -from airbyte_api.models import DestinationDuckdb from duckdb_engine import DuckDBEngineWarning from airbyte._processors.sql.duckdb import DuckDBConfig, DuckDBSqlProcessor @@ -46,10 +45,9 @@ class DuckDBCache(DuckDBConfig, CacheBase): _sql_processor_class: ClassVar[type[SqlProcessorBase]] = DuckDBSqlProcessor paired_destination_name: ClassVar[str | None] = "destination-duckdb" - paired_destination_config_class: ClassVar[type | None] = DestinationDuckdb @property - def paired_destination_config(self) -> DestinationDuckdb: + def paired_destination_config(self) -> dict[str, Any]: """Return a dictionary of destination configuration values.""" return duckdb_cache_to_destination_configuration(cache=self) diff --git a/airbyte/caches/motherduck.py b/airbyte/caches/motherduck.py index 0cb6e70bb..17d327c14 100644 --- a/airbyte/caches/motherduck.py +++ b/airbyte/caches/motherduck.py @@ -17,9 +17,8 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar -from airbyte_api.models import DestinationDuckdb from duckdb_engine import DuckDBEngineWarning from overrides import overrides from pydantic import Field @@ -74,10 +73,9 @@ class MotherDuckCache(MotherDuckConfig, DuckDBCache): _sql_processor_class: ClassVar[type[SqlProcessorBase]] = MotherDuckSqlProcessor paired_destination_name: ClassVar[str | None] = "destination-motherduck" - paired_destination_config_class: ClassVar[type | None] = DestinationDuckdb @property - def paired_destination_config(self) -> DestinationDuckdb: + def paired_destination_config(self) -> dict[str, Any]: """Return a dictionary of destination configuration values.""" return motherduck_cache_to_destination_configuration(cache=self) diff --git a/airbyte/caches/postgres.py b/airbyte/caches/postgres.py index 678baad8a..6500dc8f5 100644 --- a/airbyte/caches/postgres.py +++ b/airbyte/caches/postgres.py @@ -19,9 +19,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar - -from airbyte_api.models import DestinationPostgres +from typing import TYPE_CHECKING, Any, ClassVar from airbyte._processors.sql.postgres import PostgresConfig, PostgresSqlProcessor from airbyte.caches._utils._cache_to_dest import ( @@ -43,22 +41,22 @@ class PostgresCache(PostgresConfig, CacheBase): _sql_processor_class: ClassVar[type[SqlProcessorBase]] = PostgresSqlProcessor paired_destination_name: ClassVar[str | None] = "destination-postgres" - paired_destination_config_class: ClassVar[type | None] = DestinationPostgres @property - def paired_destination_config(self) -> DestinationPostgres: + def paired_destination_config(self) -> dict[str, Any]: """Return a dictionary of destination configuration values.""" return postgres_cache_to_destination_configuration(cache=self) - def clone_as_cloud_destination_config(self) -> DestinationPostgres: - """Return a DestinationPostgres instance with the same configuration.""" - return DestinationPostgres( - host=self.host, - port=self.port, - username=self.username, - password=self.password, - database=self.database, - ) + def clone_as_cloud_destination_config(self) -> dict[str, Any]: + """Return a cloud Postgres destination configuration with the same configuration.""" + return { + "host": self.host, + "port": self.port, + "username": self.username, + "password": self.password, + "database": self.database, + "destinationType": "postgres", + } # Expose the Cache class and also the Config class. diff --git a/airbyte/caches/snowflake.py b/airbyte/caches/snowflake.py index e878c8b3b..56267089d 100644 --- a/airbyte/caches/snowflake.py +++ b/airbyte/caches/snowflake.py @@ -59,9 +59,7 @@ from __future__ import annotations -from typing import ClassVar - -from airbyte_api.models import DestinationSnowflake +from typing import Any, ClassVar from airbyte._processors.sql.snowflake import SnowflakeConfig, SnowflakeSqlProcessor from airbyte.caches._utils._cache_to_dest import ( @@ -79,10 +77,9 @@ class SnowflakeCache(SnowflakeConfig, CacheBase): _sql_processor_class: ClassVar[type[SqlProcessorBase]] = SnowflakeSqlProcessor paired_destination_name: ClassVar[str | None] = "destination-snowflake" - paired_destination_config_class: ClassVar[type | None] = DestinationSnowflake @property - def paired_destination_config(self) -> DestinationSnowflake: + def paired_destination_config(self) -> dict[str, Any]: """Return a dictionary of destination configuration values.""" return snowflake_cache_to_destination_configuration(cache=self) diff --git a/airbyte/cloud/client.py b/airbyte/cloud/client.py index fc8639340..d90c68fd5 100644 --- a/airbyte/cloud/client.py +++ b/airbyte/cloud/client.py @@ -115,6 +115,13 @@ def _organization_has_feature( return organization.is_feature_enabled(feature) +def _api_record_to_dict(record: Any) -> dict[str, Any]: # noqa: ANN401 + """Dump a typed API read model to its JSON-mapped dict, passing dicts through.""" + if isinstance(record, dict): + return record + return record.model_dump(mode="json", by_alias=True, exclude_none=True) + + @dataclass(init=False, kw_only=True) class CloudClient: """Authenticated client for Airbyte Cloud and self-managed Airbyte APIs.""" @@ -512,9 +519,7 @@ def matches_name(workspace_name: str) -> bool: page_size=page_size, ) return [ - CloudWorkspaceInfo.from_mapping( - workspace.model_dump(mode="json", by_alias=True, exclude_none=True) - ) + CloudWorkspaceInfo.from_mapping(_api_record_to_dict(workspace)) for workspace in workspaces ] @@ -537,16 +542,14 @@ def _list_workspaces_in_organizations( organization_id=organization_id, api_root=self.public_api_root, config_api_root=self.config_api_root, - client_id=client_id, + client_id=self.client_id, client_secret=self.client_secret, bearer_token=self._get_config_api_bearer_token(), name_contains=name_contains or name, limit=None if name is not None or name_filter is not None else remaining_limit, ) organization_workspaces = [ - CloudWorkspaceInfo.from_mapping( - workspace.model_dump(mode="json", by_alias=True, exclude_none=True) - ) + CloudWorkspaceInfo.from_mapping(_api_record_to_dict(workspace)) for workspace in workspaces ] if name is not None: @@ -631,7 +634,8 @@ def _get_workspace_parent_organization_id(self, workspace_id: str) -> str: client_secret=self.client_secret, bearer_token=self._get_config_api_bearer_token(), ) - resolved_organization_id = str(organization.organizationId) + organization_record = _api_record_to_dict(organization) + resolved_organization_id = str(organization_record.get("organizationId")) if resolved_organization_id: return resolved_organization_id raise exc.AirbyteCloudError( @@ -658,14 +662,15 @@ def _get_authenticated_user_info(self) -> dict[str, Any]: guidance="Provide either client credentials or a bearer token.", ) auth_user_id = api_util.get_user_id_from_bearer_token(bearer_token) - self._authenticated_user_info = api_util.get_user_by_auth_id( + user_info = api_util.get_user_by_auth_id( auth_user_id, api_root=self.public_api_root, config_api_root=self.config_api_root, client_id=self.client_id, client_secret=self.client_secret, bearer_token=bearer_token, - ).model_dump(mode="json", by_alias=True, exclude_none=True) + ) + self._authenticated_user_info = _api_record_to_dict(user_info) return self._authenticated_user_info def _get_authenticated_user_id(self) -> str: @@ -733,7 +738,7 @@ def _get_user_permissions(self) -> tuple[dict[str, Any], ...]: """Get and cache permissions for the authenticated user.""" if self._user_permissions is None: self._user_permissions = tuple( - permission + _api_record_to_dict(permission) for permission in api_util.list_permissions_for_user( self._get_authenticated_user_id(), api_root=self.public_api_root, @@ -742,7 +747,7 @@ def _get_user_permissions(self) -> tuple[dict[str, Any], ...]: client_secret=self.client_secret, bearer_token=self._get_config_api_bearer_token(), ) - if isinstance(permission, dict) + if isinstance(permission, dict) or hasattr(permission, "model_dump") ) return self._user_permissions @@ -754,9 +759,7 @@ def _get_membership_organization_ids(self) -> tuple[str, ...]: permissions = self._get_user_permissions() organization_ids: list[str] = [] for permission in permissions: - permission_organization_id = ( - str(permission.organizationId) if permission.organizationId is not None else None - ) + permission_organization_id = permission.get("organizationId") if permission_organization_id and permission_organization_id not in organization_ids: organization_ids.append(permission_organization_id) self._membership_organization_ids = tuple(organization_ids) @@ -810,15 +813,16 @@ def _get_workspace_organization(self, workspace_id: str) -> CloudOrganizationInf # organization unknown. self._workspace_organizations[workspace_id] = None return None - organization_id = organization.get("organizationId") + organization_record = _api_record_to_dict(organization) + organization_id = organization_record.get("organizationId") if not isinstance(organization_id, str) or not organization_id: self._workspace_organizations[workspace_id] = None return None organization_info = CloudOrganizationInfo( # pyrefly: ignore[missing-argument] organization_id=organization_id, organization_name=( - organization.get("organizationName") - if isinstance(organization.get("organizationName"), str) + organization_record.get("organizationName") + if isinstance(organization_record.get("organizationName"), str) else None ), ) @@ -1136,7 +1140,7 @@ def _get_organization_candidates( except AirbyteCloudError: pass else: - organization_name = organization_info.organizationName + organization_name = _api_record_to_dict(organization_info).get("organizationName") candidates.append( { "organization_id": organization_id, @@ -1269,7 +1273,7 @@ def _fetch_organizations(self) -> list[CloudOrganization]: """Fetch all organizations available to this client.""" return [ CloudOrganization( - organization_id=organization.organization_id, + organization_id=str(organization.organization_id), organization_name=organization.organization_name, email=organization.email, client_id=self.client_id, diff --git a/airbyte/cloud/connectors.py b/airbyte/cloud/connectors.py index b4603b395..3e713d33f 100644 --- a/airbyte/cloud/connectors.py +++ b/airbyte/cloud/connectors.py @@ -57,7 +57,7 @@ from http import HTTPStatus from pathlib import Path -from typing import TYPE_CHECKING, Any, ClassVar, Literal, Protocol +from typing import TYPE_CHECKING, Any, ClassVar, Literal, Protocol, cast import requests import yaml @@ -237,9 +237,9 @@ def connector_type(self) -> ConnectorType: bearer_token=self.workspace.bearer_token, ) self._connector_info = ( - CloudSourceInfo.from_api_response(response) + CloudSourceInfo.from_api_response(cast(Any, response)) if self._connector_type is ConnectorType.SOURCE - else CloudDestinationInfo.from_api_response(response) + else CloudDestinationInfo.from_api_response(cast(Any, response)) ) return self._connector_type @@ -1344,13 +1344,17 @@ def connector_builder_project_id(self) -> str | None: bearer_token=self.workspace.bearer_token, config_api_root=self.workspace.config_api_root, ) - self._connector_builder_project_id = str(result.builderProjectId) + builder_project_id = ( + result.get("builderProjectId") if isinstance(result, dict) else result.builderProjectId + ) + workspace_id = result.get("workspaceId") if isinstance(result, dict) else result.workspaceId + self._connector_builder_project_id = ( + str(builder_project_id) if builder_project_id is not None else None + ) self._connector_builder_project_id_fetched = True # The builder project may live in a different workspace than the caller's. # We must use the project's owning workspace ID when fetching its data. - self._builder_project_workspace_id = ( - str(result.workspaceId) if result.workspaceId is not None else None - ) + self._builder_project_workspace_id = str(workspace_id) if workspace_id is not None else None return self._connector_builder_project_id @@ -1414,7 +1418,7 @@ def get_builder_project_data( }, ) - self._builder_project_data = api_util.get_connector_builder_project( + project = api_util.get_connector_builder_project( workspace_id=self._builder_project_workspace_id or self.workspace.workspace_id, builder_project_id=builder_project_id, api_root=self.workspace.api_root, @@ -1422,7 +1426,12 @@ def get_builder_project_data( client_secret=self.workspace.client_secret, bearer_token=self.workspace.bearer_token, config_api_root=self.workspace.config_api_root, - ).model_dump(mode="json", by_alias=True, exclude_none=True) + ) + self._builder_project_data = ( + project + if isinstance(project, dict) + else project.model_dump(mode="json", by_alias=True, exclude_none=True) + ) return self._builder_project_data @property diff --git a/airbyte/cloud/models.py b/airbyte/cloud/models.py index be33423e1..1d697c6c6 100644 --- a/airbyte/cloud/models.py +++ b/airbyte/cloud/models.py @@ -6,7 +6,7 @@ from collections.abc import Mapping from dataclasses import asdict, dataclass, is_dataclass from enum import Enum -from typing import Any, Literal, Protocol +from typing import Any, Literal, Protocol, cast from pydantic import BaseModel, ConfigDict, Field @@ -28,8 +28,7 @@ class _WorkspaceResponseLike(Protocol): workspace_id: str name: str data_residency: str - organization_id: str | None - notifications: object + notifications: Any class _ScheduleResponseLike(Protocol): @@ -46,26 +45,44 @@ class _ConnectionResponseLike(Protocol): name: str configurations: Any prefix: str | None - namespace_definition: object | None + namespace_definition: Any namespace_format: str | None - schedule: _ScheduleResponseLike | None - status: object + schedule: Any + status: Any class _JobResponseLike(Protocol): job_id: int - status: object + status: Any bytes_synced: int | None rows_synced: int | None start_time: str +def _missing_str_enum_member(cls: type, value: object) -> object | None: + """`Enum._missing_` factory: accept unknown string values as pseudo-members. + + API-fed enums must tolerate values newer than this library's build, so an + unrecognized status decodes into a pseudo-member holding the raw value + instead of raising `ValueError`. + """ + if not isinstance(value, str): + return None + member = cast(Any, str.__new__(cls, value)) + member._name_ = value.upper() + member._value_ = value + return member + + class ConnectionStatus(StrEnum): """Mirrors the API's connection status.""" ACTIVE = "active" INACTIVE = "inactive" DEPRECATED = "deprecated" + LOCKED = "locked" + + _missing_ = classmethod(_missing_str_enum_member) class ConnectorFeature(StrEnum): @@ -131,8 +148,8 @@ class _DestinationResponseLike(Protocol): class _DeclarativeSourceDefinitionResponseLike(Protocol): id: str name: str - manifest: dict[str, Any] | None - version: object + manifest: Any + version: Any @dataclass @@ -173,6 +190,9 @@ class JobStatusEnum(str, Enum): FAILED = "failed" SUCCEEDED = "succeeded" CANCELLED = "cancelled" + QUEUED = "queued" + + _missing_ = classmethod(_missing_str_enum_member) class JobTypeEnum(StrEnum): @@ -183,6 +203,8 @@ class JobTypeEnum(StrEnum): REFRESH = "refresh" CLEAR = "clear" + _missing_ = classmethod(_missing_str_enum_member) + class WorkspacePrivilegeScope(str, Enum): """How broadly `list_workspaces` searches for workspaces.""" @@ -571,6 +593,13 @@ def _configuration_dict(configuration: object) -> dict[str, Any] | None: return {str(key): value for key, value in asdict(configuration).items()} if isinstance(configuration, Mapping): return {str(key): value for key, value in configuration.items()} + if isinstance(configuration, BaseModel): + root_value = getattr(configuration, "root", configuration) + if isinstance(root_value, Mapping): + return {str(key): value for key, value in root_value.items()} + dumped = configuration.model_dump(mode="json") + if isinstance(dumped, Mapping): + return {str(key): value for key, value in dumped.items()} return None diff --git a/airbyte/cloud/organizations.py b/airbyte/cloud/organizations.py index 10cd2ce7e..c9f8aecff 100644 --- a/airbyte/cloud/organizations.py +++ b/airbyte/cloud/organizations.py @@ -5,7 +5,7 @@ import logging from functools import cached_property -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import requests @@ -124,7 +124,12 @@ def get_billing_status(self) -> CloudOrganizationBillingInfo: message="Failed to retrieve organization billing information.", context={"organization_id": self.organization_id}, ) from ex - billing = info.get("billing") + info_record = ( + info + if isinstance(info, dict) + else info.model_dump(mode="json", by_alias=True, exclude_none=True) + ) + billing = info_record.get("billing") if not isinstance(billing, dict): raise AirbyteCloudError( message="Organization info did not include billing details.", diff --git a/airbyte/cloud/workspaces.py b/airbyte/cloud/workspaces.py index 46beb4d65..ebff3009b 100644 --- a/airbyte/cloud/workspaces.py +++ b/airbyte/cloud/workspaces.py @@ -75,8 +75,9 @@ if TYPE_CHECKING: from collections.abc import Callable - from airbyte.cloud.organizations import CloudOrganization from airbyte_server_models._config_api import OrganizationInfoRead + + from airbyte.cloud.organizations import CloudOrganization from airbyte.sources.base import Source diff --git a/airbyte/registry.py b/airbyte/registry.py index 16f0e92ed..24e29623a 100644 --- a/airbyte/registry.py +++ b/airbyte/registry.py @@ -19,7 +19,6 @@ from airbyte import exceptions as exc from airbyte._registry_utils import fetch_registry_version_date, parse_changelog_html -from airbyte._util.compat import StrEnum from airbyte._util.meta import is_docker_installed from airbyte.constants import AIRBYTE_OFFLINE_MODE from airbyte.logs import _warn_once diff --git a/pyproject.toml b/pyproject.toml index e4f03cd2e..3b3df2353 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,6 @@ authors = [ requires-python = ">=3.11,<3.13" dynamic = ["version"] dependencies = [ - "airbyte-api>=0.53.0,<1.0", "airbyte-cdk>=7.28.3,<8.0.0", "airbyte-protocol-models-pdv2>=0.13.0,<1.0", "airbyte-server-models", @@ -120,7 +119,7 @@ allow-direct-references = true # unreleased. Pin this to a published PyPI version (and remove this source entry) # before the next PyAirbyte release. # CI authenticates to the private repo via .github/actions/private-git-deps-auth (Octavia bot app). -airbyte-server-models = { git = "https://github.com/airbytehq/airbyte-ops-mcp.git", branch = "devin/1788977312-server-models", subdirectory = "airbyte-server-models" } +airbyte-server-models = { git = "https://github.com/airbytehq/airbyte-ops-mcp.git", rev = "9a6e0ec03a96d3ac4d7bd83550ddad19da3fe96b", subdirectory = "airbyte-server-models" } [tool.hatch.build.targets.wheel] packages = ["airbyte"] diff --git a/tests/integration_tests/cloud/conftest.py b/tests/integration_tests/cloud/conftest.py index a85a33b1f..11d369a0a 100644 --- a/tests/integration_tests/cloud/conftest.py +++ b/tests/integration_tests/cloud/conftest.py @@ -19,12 +19,6 @@ from airbyte.secrets.google_gsm import GoogleGSMSecretManager from airbyte.sources.base import Source from airbyte.sources.util import get_source -from airbyte_api.models import ( - DestinationBigquery, - DestinationDuckdb, - DestinationPostgres, - DestinationSnowflake, -) AIRBYTE_CLOUD_WORKSPACE_ID = "19d7a891-8e0e-40ac-8a8c-5faf8d11e47c" @@ -135,9 +129,7 @@ def deployable_dummy_destination( @pytest.fixture(scope="function") def new_deployable_destination( request, -) -> ( - DestinationDuckdb | DestinationPostgres | DestinationBigquery | DestinationSnowflake -): +) -> dict: """This is a placeholder fixture that will be overridden by pytest_generate_tests().""" return request.getfixturevalue(request.param) diff --git a/tests/integration_tests/cloud/test_cloud_api_util.py b/tests/integration_tests/cloud/test_cloud_api_util.py index 4ffaef657..62af50b55 100644 --- a/tests/integration_tests/cloud/test_cloud_api_util.py +++ b/tests/integration_tests/cloud/test_cloud_api_util.py @@ -21,10 +21,8 @@ ) from airbyte.cloud.connectors import ConnectorType from airbyte.secrets.base import SecretString -from airbyte_api.models import ( - DestinationDuckdb, +from airbyte_server_models.public_api.models import ( DestinationResponse, - SourceFaker, SourceResponse, WorkspaceResponse, ) @@ -112,7 +110,7 @@ def test_create_and_delete_source( airbyte_cloud_client_secret: SecretString, ) -> None: new_resource_name = "deleteme-source-faker" + text_util.generate_random_suffix() - source_config = SourceFaker() + source_config = {"seed": None} source = api_util.create_source( name=new_resource_name, workspace_id=workspace_id, @@ -146,10 +144,10 @@ def test_create_and_delete_destination( new_resource_name = ( "deleteme-destination-faker" + text_util.generate_random_suffix() ) - destination_config = DestinationDuckdb( - destination_path="temp_db", - motherduck_api_key=motherduck_api_key, - ) + destination_config = { + "destination_path": "temp_db", + "motherduck_api_key": motherduck_api_key, + } destination = api_util.create_destination( name=new_resource_name, @@ -192,7 +190,7 @@ def test_create_and_delete_connection( name=new_source_name, api_root=airbyte_cloud_api_root, workspace_id=workspace_id, - config=SourceFaker(), + config={"seed": None}, client_id=airbyte_cloud_client_id, client_secret=airbyte_cloud_client_secret, bearer_token=None, @@ -205,10 +203,10 @@ def test_create_and_delete_connection( name=new_destination_name, api_root=airbyte_cloud_api_root, workspace_id=workspace_id, - config=DestinationDuckdb( - destination_path="temp_db", - motherduck_api_key=motherduck_api_key, - ), + config={ + "destination_path": "temp_db", + "motherduck_api_key": motherduck_api_key, + }, client_id=airbyte_cloud_client_id, client_secret=airbyte_cloud_client_secret, bearer_token=None, diff --git a/tests/unit_tests/test_cache_to_dest.py b/tests/unit_tests/test_cache_to_dest.py new file mode 100644 index 000000000..76cbfeea6 --- /dev/null +++ b/tests/unit_tests/test_cache_to_dest.py @@ -0,0 +1,121 @@ +# Copyright (c) 2026 Airbyte, Inc., all rights reserved. +"""Golden tests for cache-to-cloud-destination configuration dicts. + +These freeze the exact wire JSON the removed Speakeasy `airbyte_api` destination +config models serialized to, so the dict-based replacements stay byte-identical. +""" + +from __future__ import annotations + +import pytest +from airbyte.caches.bigquery import BigQueryCache +from airbyte.caches.duckdb import DuckDBCache +from airbyte.caches.motherduck import MotherDuckCache +from airbyte.caches.postgres import PostgresCache +from airbyte.caches.snowflake import SnowflakeCache + +from airbyte.caches._utils import _cache_to_dest + + +@pytest.mark.parametrize( + ("cache", "expected"), + [ + pytest.param( + DuckDBCache.model_construct(db_path="/tmp/db.duckdb", schema_name="main"), + { + "destination_path": "/tmp/db.duckdb", + "destinationType": "duckdb", + "schema": "main", + }, + id="duckdb", + ), + pytest.param( + MotherDuckCache.model_construct( + db_path="md:db", schema_name="main", api_key="token" + ), + { + "destination_path": "md:db", + "destinationType": "duckdb", + "motherduck_api_key": "token", + "schema": "main", + }, + id="motherduck", + ), + pytest.param( + PostgresCache.model_construct( + database="db", + host="host", + username="user", + password="pass", + port=5432, + schema_name="public", + ), + { + "database": "db", + "host": "host", + "username": "user", + "destinationType": "postgres", + "disable_type_dedupe": False, + "drop_cascade": False, + "password": "pass", + "port": 5432, + "schema": "public", + "ssl": False, + "unconstrained_number": False, + }, + id="postgres", + ), + pytest.param( + SnowflakeCache.model_construct( + account="acct", + database="db", + warehouse="wh", + role="role", + username="user", + password="pass", + schema_name="public", + ), + { + "host": "acct.snowflakecomputing.com", + "database": "DB", + "schema": "PUBLIC", + "warehouse": "wh", + "role": "role", + "username": "user", + "credentials": { + "password": "pass", + "auth_type": "Username and Password", + }, + "destinationType": "snowflake", + "disable_type_dedupe": False, + "retention_period_days": 1, + "use_merge_for_upsert": False, + }, + id="snowflake", + ), + pytest.param( + BigQueryCache.model_construct( + project_name="proj", + dataset_name="ds", + dataset_location="US", + credentials_path=None, + ), + { + "project_id": "proj", + "dataset_id": "ds", + "dataset_location": "US", + "cdc_deletion_mode": "Hard delete", + "credentials_json": None, + "destinationType": "bigquery", + "disable_type_dedupe": False, + "loading_method": {"method": "Standard"}, + }, + id="bigquery", + ), + ], +) +def test_cache_to_destination_configuration_matches_speakeasy_wire_json( + cache: object, + expected: dict, +) -> None: + assert _cache_to_dest.cache_to_destination_configuration(cache) == expected diff --git a/tests/unit_tests/test_cloud_api_util.py b/tests/unit_tests/test_cloud_api_util.py index dafaab90e..9459de2d0 100644 --- a/tests/unit_tests/test_cloud_api_util.py +++ b/tests/unit_tests/test_cloud_api_util.py @@ -17,9 +17,10 @@ AirbyteWorkspaceNotEmptyError, AirbyteLibInputError, ) +from airbyte.cloud.models import CloudConnectionInfo, CloudJobInfo, JobStatusEnum from airbyte.registry import ConnectorType from airbyte.secrets.base import SecretString -from airbyte_api import api, models +from airbyte_server_models.public_api import models from airbyte_server_models._config_api import ( CheckConnectionRead, JobConfigType, @@ -43,9 +44,6 @@ def _id_for_index(index: int) -> UUID: return UUID(int=index + 10) -from airbyte_api.errors import SDKError - - def _job_response(job_id: int) -> models.JobResponse: """Create a minimal job response for pagination tests.""" return models.JobResponse( @@ -57,28 +55,14 @@ def _job_response(job_id: int) -> models.JobResponse: ) -def _list_jobs_response( - data: list[models.JobResponse], - *, - next_page: str | None, -) -> api.ListJobsResponse: - """Create a paginated jobs API response.""" - raw_response = requests.Response() - raw_response.url = "https://api.airbyte.com/v1/jobs" - return api.ListJobsResponse( - content_type="application/json", - status_code=200, - raw_response=raw_response, - jobs_response=models.JobsResponse( - data=data, - next=next_page, - ), - ) +def _job_wire(job_id: int) -> dict[str, object]: + """Create a minimal job wire dict for pagination tests.""" + return _job_response(job_id).model_dump(mode="json", by_alias=True) def _connection_response(name: str, index: int) -> models.ConnectionResponse: """Create a minimal connection response for pagination tests.""" - return models.ConnectionResponse( + return models.ConnectionResponse.model_construct( configurations={}, connection_id=f"connection-{index}", created_at=index, @@ -92,6 +76,23 @@ def _connection_response(name: str, index: int) -> models.ConnectionResponse: ) +def _connection_wire(name: str, index: int) -> dict[str, object]: + """Create a minimal connection wire dict for pagination tests.""" + return { + "connectionId": f"connection-{index}", + "name": name, + "sourceId": f"source-{index}", + "destinationId": f"destination-{index}", + "workspaceId": "workspace-id", + "status": "active", + "schedule": {"scheduleType": "manual"}, + "nonBreakingSchemaUpdatesBehavior": "ignore", + "configurations": {"streams": []}, + "createdAt": index, + "tags": [], + } + + def _workspace_response(name: str, index: int) -> models.WorkspaceResponse: """Create a minimal workspace response for pagination tests.""" return models.WorkspaceResponse( @@ -102,42 +103,46 @@ def _workspace_response(name: str, index: int) -> models.WorkspaceResponse: ) -def _list_connections_response( - data: list[models.ConnectionResponse], - *, - next_page: str | None, -) -> api.ListConnectionsResponse: - """Create a paginated connections API response.""" - raw_response = requests.Response() - raw_response.url = "https://api.airbyte.com/v1/connections" - return api.ListConnectionsResponse( - content_type="application/json", - status_code=200, - raw_response=raw_response, - connections_response=models.ConnectionsResponse( - data=data, - next=next_page, - ), - ) +def _workspace_wire(name: str, index: int) -> dict[str, object]: + """Create a minimal workspace wire dict for pagination tests.""" + return _workspace_response(name, index).model_dump(mode="json", by_alias=True) -def _list_workspaces_response( - data: list[models.WorkspaceResponse], - *, - next_page: str | None, -) -> api.ListWorkspacesResponse: - """Create a paginated workspaces API response.""" - raw_response = requests.Response() - raw_response.url = "https://api.airbyte.com/v1/workspaces" - return api.ListWorkspacesResponse( - content_type="application/json", - status_code=200, - raw_response=raw_response, - workspaces_response=models.WorkspacesResponse( - data=data, - next=next_page, - ), - ) +def _page_body(items: list[dict[str, object]], next_page: str | None = None) -> dict: + """Create a paged list-endpoint wire body.""" + return {"data": items, "next": next_page} + + +def _public_api_spy( + monkeypatch: pytest.MonkeyPatch, + results: list[object], +) -> list[dict[str, object]]: + """Patch `_make_public_api_request`, capturing call kwargs and returning queued results.""" + captured: list[dict[str, object]] = [] + pages = list(results) + + def fake_request(**kwargs: object) -> object: + captured.append(kwargs) + result = pages.pop(0) + response_model = kwargs.get("response_model") + if response_model is not None and isinstance(result, dict): + return response_model.model_validate(result) + return result + + monkeypatch.setattr(api_util, "_make_public_api_request", fake_request) + return captured + + +def _http_error_response( + status_code: int, *, url: str, text: str = "" +) -> requests.Response: + """Build a `requests.Response` like a failed Public API call.""" + response = requests.Response() + response.status_code = status_code + response.url = url + response._content = text.encode() + response.request = requests.Request("GET", url).prepare() + return response @pytest.mark.parametrize( @@ -167,20 +172,19 @@ def _list_workspaces_response( ), ], ) -def test_wrap_sdk_error_classifies_missing_or_forbidden( +def test_wrap_api_error_classifies_missing_or_forbidden( status_code: int, expected_error_type: type[AirbyteCloudError], expected_message: str, expected_guidance: str | None, ) -> None: - raw_response = requests.Response() - raw_response.status_code = status_code - raw_response.url = "https://api.airbyte.com/v1/workspaces/workspace-id" - error = SDKError( - "Workspace lookup failed.", status_code, "response body", raw_response + raw_response = _http_error_response( + status_code, + url="https://api.airbyte.com/v1/workspaces/workspace-id", + text="Workspace lookup failed.", ) - wrapped = api_util._wrap_sdk_error(error, {"workspace_id": "workspace-id"}) + wrapped = api_util._wrap_api_error(raw_response, {"workspace_id": "workspace-id"}) assert type(wrapped) is expected_error_type assert wrapped.get_message() == expected_message @@ -305,7 +309,7 @@ def test_make_config_api_request_wraps_response_validation_error( monkeypatch.setattr(requests, "request", lambda **_: response) with pytest.raises( - AirbyteError, match="did not match the expected schema" + AirbyteCloudError, match="did not match the expected schema" ) as exc_info: api_util._make_config_api_request( api_root="https://api.example", @@ -340,7 +344,7 @@ def raise_json_error() -> object: monkeypatch.setattr(requests, "request", lambda **_: response) with pytest.raises( - AirbyteError, match="did not match the expected schema" + AirbyteCloudError, match="did not match the expected schema" ) as exc_info: api_util._make_config_api_request( api_root="https://api.example", @@ -580,9 +584,9 @@ def test_list_workspaces_by_user_paginates_and_respects_limit( next_workspace_id += page_length def fake_config_request(**kwargs: object) -> dict[str, object]: - json_request = kwargs["json"] + json_request = kwargs["request"] assert isinstance(json_request, dict) - captured_requests.append({"path": kwargs["path"], "json": json_request}) + captured_requests.append({"path": kwargs["path"], "request": json_request}) return pages.pop(0) monkeypatch.setattr(api_util, "_make_config_api_request", fake_config_request) @@ -604,7 +608,7 @@ def fake_config_request(**kwargs: object) -> dict[str, object]: assert [request["path"] for request in captured_requests] == [ "/workspaces/list_by_user_id" ] * len(expected_offsets) - assert [request["json"] for request in captured_requests] == [ + assert [request["request"] for request in captured_requests] == [ { "userId": "user-id", "pagination": {"pageSize": 100, "rowOffset": offset}, @@ -652,9 +656,9 @@ def test_list_workspaces_by_user_filters_each_page_before_limit( ] def fake_config_request(**kwargs: object) -> dict[str, object]: - json_request = kwargs["json"] + json_request = kwargs["request"] assert isinstance(json_request, dict) - captured_requests.append({"path": kwargs["path"], "json": json_request}) + captured_requests.append({"path": kwargs["path"], "request": json_request}) return pages.pop(0) monkeypatch.setattr(api_util, "_make_config_api_request", fake_config_request) @@ -672,7 +676,7 @@ def fake_config_request(**kwargs: object) -> dict[str, object]: assert [workspace["name"] for workspace in result] == expected_names assert [ - request["json"]["pagination"]["rowOffset"] for request in captured_requests + request["request"]["pagination"]["rowOffset"] for request in captured_requests ] == (expected_offsets) assert [request["path"] for request in captured_requests] == [ "/workspaces/list_by_user_id" @@ -685,7 +689,7 @@ def test_list_workspaces_by_user_honors_page_size( captured_payloads: list[dict[str, object]] = [] def fake_config_request(**kwargs: object) -> dict[str, object]: - json_request = kwargs["json"] + json_request = kwargs["request"] assert isinstance(json_request, dict) captured_payloads.append(json_request) return { @@ -717,36 +721,14 @@ def fake_config_request(**kwargs: object) -> dict[str, object]: def test_create_workspace_forwards_request( monkeypatch: pytest.MonkeyPatch, ) -> None: - captured_request = None - - def create_workspace( - *, - request: models.WorkspaceCreateRequest, - ) -> api.CreateWorkspaceResponse: - nonlocal captured_request - captured_request = request - raw_response = requests.Response() - raw_response.url = "https://api.airbyte.com/v1/workspaces" - return api.CreateWorkspaceResponse( - content_type="application/json", - status_code=200, - raw_response=raw_response, - workspace_response=_workspace_response("New workspace", 1), - ) - - airbyte_instance = SimpleNamespace( - workspaces=SimpleNamespace(create_workspace=create_workspace) - ) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, - ) + captured = _public_api_spy(monkeypatch, [_workspace_response("New workspace", 1)]) + organization_id = str(_id_for_index(0)) + region_id = str(_id_for_index(1)) workspace = api_util.create_workspace( name="New workspace", - organization_id="organization-id", - region_id="us-east", + organization_id=organization_id, + region_id=region_id, api_root="https://api.airbyte.com/v1", client_id=None, client_secret=None, @@ -754,38 +736,19 @@ def create_workspace( ) assert workspace.workspace_id == "workspace-1" - assert captured_request is not None + assert captured[0]["method"] == "POST" + assert captured[0]["path"] == "/workspaces" + captured_request = captured[0]["request"] assert captured_request.name == "New workspace" - assert captured_request.organization_id == "organization-id" - assert captured_request.region_id == "us-east" + assert str(captured_request.organization_id) == organization_id + assert str(captured_request.region_id) == region_id def test_rename_workspace_forwards_request( monkeypatch: pytest.MonkeyPatch, ) -> None: - captured_request = None - - def update_workspace( - request: api.UpdateWorkspaceRequest, - ) -> api.UpdateWorkspaceResponse: - nonlocal captured_request - captured_request = request - raw_response = requests.Response() - raw_response.url = "https://api.airbyte.com/v1/workspaces/workspace-1" - return api.UpdateWorkspaceResponse( - content_type="application/json", - status_code=200, - raw_response=raw_response, - workspace_response=_workspace_response("Renamed workspace", 1), - ) - - airbyte_instance = SimpleNamespace( - workspaces=SimpleNamespace(update_workspace=update_workspace) - ) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, + captured = _public_api_spy( + monkeypatch, [_workspace_response("Renamed workspace", 1)] ) workspace = api_util.rename_workspace( @@ -798,39 +761,16 @@ def update_workspace( ) assert workspace.name == "Renamed workspace" - assert captured_request is not None - assert captured_request.workspace_id == "workspace-1" - assert captured_request.workspace_update_request.name == "Renamed workspace" + assert captured[0]["method"] == "PATCH" + assert captured[0]["path"] == "/workspaces/workspace-1" + assert captured[0]["request"].name == "Renamed workspace" def test_patch_connection_normalizes_status_string( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Verify string connection statuses are normalized for the generated SDK.""" - captured_request = None - - def patch_connection( - request: api.PatchConnectionRequest, - ) -> api.PatchConnectionResponse: - nonlocal captured_request - captured_request = request - raw_response = requests.Response() - raw_response.url = "https://api.airbyte.com/v1/connections/connection-1" - return api.PatchConnectionResponse( - content_type="application/json", - status_code=200, - raw_response=raw_response, - connection_response=_connection_response("Connection", 1), - ) - - airbyte_instance = SimpleNamespace( - connections=SimpleNamespace(patch_connection=patch_connection) - ) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, - ) + """Verify string connection statuses are normalized for the API request model.""" + captured = _public_api_spy(monkeypatch, [_connection_response("Connection", 1)]) api_util.patch_connection( connection_id="connection-1", @@ -841,22 +781,17 @@ def patch_connection( status="inactive", ) - assert captured_request is not None - assert ( - captured_request.connection_patch_request.status - == models.ConnectionStatusEnum.INACTIVE - ) + assert captured[0]["request"].status == models.ConnectionStatusEnum.INACTIVE def test_patch_connection_rejects_invalid_status( monkeypatch: pytest.MonkeyPatch, ) -> None: """Verify invalid string connection statuses produce PyAirbyte errors.""" - airbyte_instance = SimpleNamespace(connections=SimpleNamespace()) monkeypatch.setattr( api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, + "_make_public_api_request", + Mock(side_effect=AssertionError("API should not be called")), ) with pytest.raises(AirbyteLibInputError, match="`status` must be one of"): @@ -883,7 +818,7 @@ def test_permanently_delete_workspace_requires_safe_name( workspace_name: str, should_delete: bool, ) -> None: - delete_calls = 0 + delete_calls = [] def get_workspace(**_: object) -> models.WorkspaceResponse: return models.WorkspaceResponse( @@ -893,29 +828,13 @@ def get_workspace(**_: object) -> models.WorkspaceResponse: workspace_id="workspace-1", ) - def delete_workspace( - request: api.DeleteWorkspaceRequest, - ) -> api.DeleteWorkspaceResponse: - nonlocal delete_calls - delete_calls += 1 - assert request.workspace_id == "workspace-1" - raw_response = requests.Response() - raw_response.url = "https://api.airbyte.com/v1/workspaces/workspace-1" - return api.DeleteWorkspaceResponse( - content_type="", - status_code=204, - raw_response=raw_response, - ) + def fake_request(**kwargs: object) -> None: + if kwargs["method"] == "DELETE": + delete_calls.append(kwargs["path"]) + return None - airbyte_instance = SimpleNamespace( - workspaces=SimpleNamespace(delete_workspace=delete_workspace) - ) monkeypatch.setattr(api_util, "get_workspace", get_workspace) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, - ) + monkeypatch.setattr(api_util, "_make_public_api_request", fake_request) monkeypatch.setattr(api_util, "list_connections", lambda **_: []) if should_delete: @@ -926,7 +845,7 @@ def delete_workspace( client_secret=None, bearer_token=SecretString("token"), ) - assert delete_calls == 1 + assert delete_calls == ["/workspaces/workspace-1"] else: with pytest.raises(AirbyteLibInputError): api_util.permanently_delete_workspace( @@ -936,34 +855,17 @@ def delete_workspace( client_secret=None, bearer_token=SecretString("token"), ) - assert delete_calls == 0 + assert delete_calls == [] def test_permanently_delete_workspace_requires_empty_workspace( monkeypatch: pytest.MonkeyPatch, ) -> None: - delete_calls = 0 - - def delete_workspace( - request: api.DeleteWorkspaceRequest, - ) -> api.DeleteWorkspaceResponse: - nonlocal delete_calls - delete_calls += 1 - raw_response = requests.Response() - raw_response.url = "https://api.airbyte.com/v1/workspaces/workspace-1" - return api.DeleteWorkspaceResponse( - content_type="", - status_code=204, - raw_response=raw_response, - ) - - airbyte_instance = SimpleNamespace( - workspaces=SimpleNamespace(delete_workspace=delete_workspace) - ) + delete_calls = [] monkeypatch.setattr( api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, + "_make_public_api_request", + lambda **kwargs: delete_calls.append(kwargs) or None, ) monkeypatch.setattr( api_util, @@ -983,7 +885,7 @@ def delete_workspace( assert exc_info.value.workspace_id == "workspace-id" assert exc_info.value.connection_ids == ["connection-1"] - assert delete_calls == 0 + assert delete_calls == [] @pytest.mark.parametrize( @@ -992,12 +894,11 @@ def delete_workspace( pytest.param( {"limit": 1, "name_filter": lambda name: name == "target"}, [ - _list_connections_response( + _page_body( [ - _connection_response("miss", 1), - _connection_response("target", 2), + _connection_wire("miss", 1), + _connection_wire("target", 2), ], - next_page=None, ), ], ["target"], @@ -1007,16 +908,15 @@ def delete_workspace( pytest.param( {"limit": 2, "name_filter": lambda name: name == "target"}, [ - _list_connections_response( - [_connection_response("target", 1)], + _page_body( + [_connection_wire("target", 1)], next_page="next", ), - _list_connections_response( + _page_body( [ - _connection_response("target", 2), - _connection_response("extra", 3), + _connection_wire("target", 2), + _connection_wire("extra", 3), ], - next_page=None, ), ], ["target", "target"], @@ -1026,12 +926,11 @@ def delete_workspace( pytest.param( {"name": ""}, [ - _list_connections_response( + _page_body( [ - _connection_response("", 1), - _connection_response("non-empty", 2), + _connection_wire("", 1), + _connection_wire("non-empty", 2), ], - next_page=None, ), ], [""], @@ -1041,13 +940,12 @@ def delete_workspace( pytest.param( {}, [ - _list_connections_response( - [_connection_response("first", 1)], + _page_body( + [_connection_wire("first", 1)], next_page="next", ), - _list_connections_response( - [_connection_response("second", 2)], - next_page=None, + _page_body( + [_connection_wire("second", 2)], ), ], ["first", "second"], @@ -1059,28 +957,12 @@ def delete_workspace( def test_list_connections_paginates_resources( monkeypatch: pytest.MonkeyPatch, kwargs: dict, - pages: list[api.ListConnectionsResponse], + pages: list[dict], expected_names: list[str], expected_requests: list[tuple[int | None, int | None]], ) -> None: """Verify resource list pagination, filtering, and request sizing.""" - captured_requests: list[api.ListConnectionsRequest] = [] - - def list_connections( - request: api.ListConnectionsRequest, - ) -> api.ListConnectionsResponse: - """Capture connection list requests and return queued pages.""" - captured_requests.append(request) - return pages.pop(0) - - airbyte_instance = SimpleNamespace( - connections=SimpleNamespace(list_connections=list_connections), - ) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, - ) + captured = _public_api_spy(monkeypatch, pages) result = api_util.list_connections( workspace_id="workspace-id", @@ -1093,7 +975,7 @@ def list_connections( assert [connection.name for connection in result] == expected_names assert [ - (request.limit, request.offset) for request in captured_requests + (call["params"]["limit"], call["params"]["offset"]) for call in captured ] == expected_requests @@ -1101,32 +983,12 @@ def test_list_workspaces_does_not_filter_by_workspace_id( monkeypatch: pytest.MonkeyPatch, ) -> None: """Verify workspace listing fetches accessible workspaces across pages.""" - captured_requests: list[api.ListWorkspacesRequest] = [] - pages = [ - _list_workspaces_response( - [_workspace_response("first", 1)], - next_page="next", - ), - _list_workspaces_response( - [_workspace_response("second", 2)], - next_page=None, - ), - ] - - def list_workspaces( - request: api.ListWorkspacesRequest, - ) -> api.ListWorkspacesResponse: - """Capture workspace list requests and return queued pages.""" - captured_requests.append(request) - return pages.pop(0) - - airbyte_instance = SimpleNamespace( - workspaces=SimpleNamespace(list_workspaces=list_workspaces), - ) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, + captured = _public_api_spy( + monkeypatch, + [ + _page_body([_workspace_wire("first", 1)], next_page="next"), + _page_body([_workspace_wire("second", 2)]), + ], ) result = api_util.list_workspaces( @@ -1142,40 +1004,21 @@ def list_workspaces( "workspace-2", ] assert [ - (request.workspace_ids, request.limit, request.offset) - for request in captured_requests + (call["params"]["limit"], call["params"]["offset"]) for call in captured ] == [ - (None, 100, 0), - (None, 100, 1), + (100, 0), + (100, 1), ] + assert all("workspaceIds" not in call["params"] for call in captured) def test_list_workspaces_caps_unfiltered_api_page_size( monkeypatch: pytest.MonkeyPatch, ) -> None: """Verify unfiltered workspace listing uses requested limit as API page size.""" - captured_requests: list[api.ListWorkspacesRequest] = [] - pages = [ - _list_workspaces_response( - [_workspace_response("first", 1)], - next_page="next", - ), - ] - - def list_workspaces( - request: api.ListWorkspacesRequest, - ) -> api.ListWorkspacesResponse: - """Capture workspace list requests and return queued pages.""" - captured_requests.append(request) - return pages.pop(0) - - airbyte_instance = SimpleNamespace( - workspaces=SimpleNamespace(list_workspaces=list_workspaces), - ) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, + captured = _public_api_spy( + monkeypatch, + [_page_body([_workspace_wire("first", 1)], next_page="next")], ) result = api_util.list_workspaces( @@ -1189,9 +1032,8 @@ def list_workspaces( assert [workspace.workspace_id for workspace in result] == ["workspace-1"] assert [ - (request.workspace_ids, request.limit, request.offset) - for request in captured_requests - ] == [(None, 1, 0)] + (call["params"]["limit"], call["params"]["offset"]) for call in captured + ] == [(1, 0)] @pytest.mark.parametrize("limit", [0, -1]) @@ -1210,28 +1052,12 @@ def test_list_connections_rejects_invalid_limits(limit: int) -> None: def test_get_job_logs_paginates_until_limit(monkeypatch: pytest.MonkeyPatch) -> None: """Verify job log pagination stops after collecting the requested limit.""" - captured_requests: list[api.ListJobsRequest] = [] - pages = [ - _list_jobs_response( - [_job_response(job_id) for job_id in range(100)], - next_page="next", - ), - _list_jobs_response( - [_job_response(job_id) for job_id in range(100, 150)], - next_page=None, - ), - ] - - def list_jobs(request: api.ListJobsRequest) -> api.ListJobsResponse: - """Capture job list requests and return queued pages.""" - captured_requests.append(request) - return pages.pop(0) - - airbyte_instance = SimpleNamespace(jobs=SimpleNamespace(list_jobs=list_jobs)) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, + captured = _public_api_spy( + monkeypatch, + [ + _page_body([_job_wire(job_id) for job_id in range(100)], next_page="next"), + _page_body([_job_wire(job_id) for job_id in range(100, 150)]), + ], ) result = api_util.get_job_logs( @@ -1245,7 +1071,9 @@ def list_jobs(request: api.ListJobsRequest) -> api.ListJobsResponse: ) assert [job.job_id for job in result] == list(range(150)) - assert [(request.limit, request.offset) for request in captured_requests] == [ + assert [ + (call["params"]["limit"], call["params"]["offset"]) for call in captured + ] == [ (100, 0), (50, 100), ] @@ -1255,28 +1083,12 @@ def test_get_job_logs_uses_offset_and_allows_unbounded_limit( monkeypatch: pytest.MonkeyPatch, ) -> None: """Verify job log pagination preserves offset and treats `None` as unbounded.""" - captured_requests: list[api.ListJobsRequest] = [] - pages = [ - _list_jobs_response( - [_job_response(job_id) for job_id in range(100)], - next_page="next", - ), - _list_jobs_response( - [_job_response(job_id) for job_id in range(100, 125)], - next_page=None, - ), - ] - - def list_jobs(request: api.ListJobsRequest) -> api.ListJobsResponse: - """Capture job list requests and return queued pages.""" - captured_requests.append(request) - return pages.pop(0) - - airbyte_instance = SimpleNamespace(jobs=SimpleNamespace(list_jobs=list_jobs)) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, + captured = _public_api_spy( + monkeypatch, + [ + _page_body([_job_wire(job_id) for job_id in range(100)], next_page="next"), + _page_body([_job_wire(job_id) for job_id in range(100, 125)]), + ], ) result = api_util.get_job_logs( @@ -1291,7 +1103,9 @@ def list_jobs(request: api.ListJobsRequest) -> api.ListJobsResponse: ) assert [job.job_id for job in result] == list(range(125)) - assert [(request.limit, request.offset) for request in captured_requests] == [ + assert [ + (call["params"]["limit"], call["params"]["offset"]) for call in captured + ] == [ (100, 10), (100, 110), ] @@ -1301,28 +1115,8 @@ def test_cancel_job_forwards_request_and_returns_job_response( monkeypatch: pytest.MonkeyPatch, ) -> None: """Verify cancelling a job forwards its ID and returns the API job response.""" - captured_request: api.CancelJobRequest | None = None job_response = _job_response(42) - raw_response = requests.Response() - raw_response.url = "https://api.airbyte.com/v1/jobs/42" - - def cancel_job(request: api.CancelJobRequest) -> api.CancelJobResponse: - """Capture the cancellation request.""" - nonlocal captured_request - captured_request = request - return api.CancelJobResponse( - content_type="application/json", - status_code=200, - raw_response=raw_response, - job_response=job_response, - ) - - airbyte_instance = SimpleNamespace(jobs=SimpleNamespace(cancel_job=cancel_job)) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, - ) + captured = _public_api_spy(monkeypatch, [job_response]) result = api_util.cancel_job( job_id=42, @@ -1333,32 +1127,19 @@ def cancel_job(request: api.CancelJobRequest) -> api.CancelJobResponse: ) assert result is job_response - assert captured_request is not None - assert captured_request.job_id == 42 + assert captured[0]["method"] == "DELETE" + assert captured[0]["path"] == "/jobs/42" def test_cancel_job_raises_for_non_ok_response( monkeypatch: pytest.MonkeyPatch, ) -> None: """Verify cancelling a job raises when the API response is not successful.""" - raw_response = requests.Response() - raw_response.status_code = 404 - raw_response.url = "https://api.airbyte.com/v1/jobs/42" - - def cancel_job(request: api.CancelJobRequest) -> api.CancelJobResponse: - """Return a not-found cancellation response.""" - _ = request - return api.CancelJobResponse( - content_type="application/json", - status_code=404, - raw_response=raw_response, - ) - - airbyte_instance = SimpleNamespace(jobs=SimpleNamespace(cancel_job=cancel_job)) + error_response = _http_error_response(404, url="https://api.airbyte.com/v1/jobs/42") monkeypatch.setattr( api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, + "_make_public_api_request", + Mock(side_effect=api_util._wrap_api_error(error_response)), ) with pytest.raises(AirbyteMissingResourceError): @@ -1375,24 +1156,11 @@ def test_cancel_job_raises_airbyte_error_for_non_not_found_response( monkeypatch: pytest.MonkeyPatch, ) -> None: """Verify non-not-found cancellation failures use a general API error.""" - raw_response = requests.Response() - raw_response.status_code = 409 - raw_response.url = "https://api.airbyte.com/v1/jobs/42" - - def cancel_job(request: api.CancelJobRequest) -> api.CancelJobResponse: - """Return a conflict cancellation response.""" - _ = request - return api.CancelJobResponse( - content_type="application/json", - status_code=409, - raw_response=raw_response, - ) - - airbyte_instance = SimpleNamespace(jobs=SimpleNamespace(cancel_job=cancel_job)) + error_response = _http_error_response(409, url="https://api.airbyte.com/v1/jobs/42") monkeypatch.setattr( api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, + "_make_public_api_request", + Mock(side_effect=api_util._wrap_api_error(error_response)), ) with pytest.raises(AirbyteCloudError) as error: @@ -1490,7 +1258,7 @@ def fake_request(**kwargs: object) -> SimpleNamespace: api_util._make_config_api_request( path="/workspaces/get", - json={}, + request={}, api_root="https://api.airbyte.com/v1", client_id=None, client_secret=None, @@ -1513,7 +1281,7 @@ def test_config_api_request_handles_no_content_response( result = api_util._make_config_api_request( path="/connector_builder_projects/update", - json={}, + request={}, api_root="https://api.airbyte.com/v1", config_api_root="https://config.airbyte.com/v1", client_id=None, @@ -1546,7 +1314,7 @@ def test_list_connector_builder_projects(monkeypatch: pytest.MonkeyPatch) -> Non assert result == projects config_api_request.assert_called_once_with( path="/connector_builder_projects/list", - json={"workspaceId": "workspace-id"}, + request={"workspaceId": "workspace-id"}, api_root="https://api.airbyte.com/v1", config_api_root="https://config.airbyte.com/v1", client_id=None, @@ -1601,7 +1369,7 @@ def test_update_connector_builder_project_payload( assert result is None config_api_request.assert_called_once_with( path="/connector_builder_projects/update", - json={ + request={ "workspaceId": "owner-workspace", "builderProjectId": "builder-project-id", "builderProject": expected_builder_project, @@ -1651,7 +1419,7 @@ def test_config_api_request_maps_forbidden_as_missing_resource( with pytest.raises(expected_error_type) as exc_info: api_util._make_config_api_request( path="/workspaces/get", - json={"workspaceId": "workspace-id"}, + request={"workspaceId": "workspace-id"}, api_root="https://api.airbyte.com/v1", config_api_root="https://config.airbyte.com/v1", client_id=None, @@ -1678,21 +1446,42 @@ def test_config_api_request_maps_forbidden_as_missing_resource( ) -def test_public_api_client_sends_analytic_source_header( +def test_public_api_request_sends_bearer_and_analytic_source_headers( monkeypatch: pytest.MonkeyPatch, ) -> None: + """The Public API path sends bearer auth plus the analytic source header.""" monkeypatch.setattr(meta, "_MCP_MODE_ENABLED", True) monkeypatch.setattr(constants, "_HOSTED_MCP_MODE_ENABLED", False) + captured: dict[str, object] = {} + + def fake_request(**kwargs: object) -> SimpleNamespace: + captured.update(kwargs) + return SimpleNamespace( + status_code=200, + content=b"{}", + json=lambda: {}, + text="{}", + url="https://api.airbyte.com/v1/x", + headers={}, + request=SimpleNamespace(url="https://api.airbyte.com/v1/x", method="GET"), + ) + + monkeypatch.setattr(api_util.requests, "request", fake_request) - airbyte_instance = api_util.get_airbyte_server_instance( + api_util._make_public_api_request( + method="GET", api_root="https://api.airbyte.com/v1", + path="/x", + response_model=None, client_id=None, client_secret=None, bearer_token=SecretString("token"), ) - session = airbyte_instance.sdk_configuration.client - assert session.headers[meta.AIRBYTE_ANALYTIC_SOURCE_HEADER] == "pyairbyte-mcp-local" + headers = captured["headers"] + assert isinstance(headers, dict) + assert headers["Authorization"] == "Bearer token" + assert headers[meta.AIRBYTE_ANALYTIC_SOURCE_HEADER] == "pyairbyte-mcp-local" def test_get_bearer_token_sends_analytic_source_header( @@ -1718,29 +1507,25 @@ def fake_post(**kwargs: object) -> SimpleNamespace: assert headers[meta.AIRBYTE_ANALYTIC_SOURCE_HEADER] == "pyairbyte" -def _sdk_404_error(resource_type: str) -> SDKError: - """Create an SDKError like the Speakeasy SDK raises on a 404.""" - raw_response = requests.Response() - raw_response.status_code = 404 - raw_response.url = "https://api.airbyte.com/v1/connectors/connector-id" - return SDKError( - "API error occurred: Status 404", - 404, - f'{{"resourceType":"{resource_type}"}}', - raw_response, +def _api_404_error() -> AirbyteCloudError: + """Wrap a failed Public API response like a 404 connector lookup.""" + return api_util._wrap_api_error( + _http_error_response( + 404, + url="https://api.airbyte.com/v1/connectors/connector-id", + text="not found", + ) ) -def _sdk_status_error(status_code: int) -> SDKError: - """Create an SDKError like the Speakeasy SDK raises on a non-2xx status.""" - raw_response = requests.Response() - raw_response.status_code = status_code - raw_response.url = "https://api.airbyte.com/v1/connectors/connector-id" - return SDKError( - f"API error occurred: Status {status_code}", - status_code, - '{"message":"Caller does not have the required permissions"}', - raw_response, +def _api_status_error(status_code: int) -> AirbyteCloudError: + """Wrap a failed Public API response for a non-2xx status.""" + return api_util._wrap_api_error( + _http_error_response( + status_code, + url="https://api.airbyte.com/v1/connectors/connector-id", + text='{"message":"Caller does not have the required permissions"}', + ) ) @@ -1751,23 +1536,28 @@ def _sdk_status_error(status_code: int) -> SDKError: pytest.param("patch-connection", 400, id="patch-connection"), ], ) -def test_api_util_calls_wrap_sdk_errors_with_status_context( +def test_api_util_calls_wrap_api_errors_with_status_context( monkeypatch: pytest.MonkeyPatch, operation: str, status_code: int, ) -> None: - """SDK errors from job and connection calls retain status and request context.""" - sdk_error = _sdk_status_error(status_code) - get_job = Mock(side_effect=sdk_error) - patch_connection = Mock(side_effect=sdk_error) - airbyte_instance = SimpleNamespace( - jobs=SimpleNamespace(get_job=get_job), - connections=SimpleNamespace(patch_connection=patch_connection), - ) + """Errors from job and connection calls retain status and request context.""" + api_error = _api_status_error(status_code) + + def fake_request(**kwargs: object) -> object: + context = dict(api_error.context or {}) + context.update(kwargs.get("error_context") or {}) + error_cls = ( + api_util.AirbyteMissingResourceError + if status_code == 403 + else api_util.AirbyteCloudError + ) + raise error_cls(message=api_error.get_message(), context=context) + monkeypatch.setattr( api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, + "_make_public_api_request", + fake_request, ) with pytest.raises(AirbyteCloudError) as exc_info: @@ -1790,17 +1580,12 @@ def test_api_util_calls_wrap_sdk_errors_with_status_context( assert exc_info.value.context is not None assert exc_info.value.context["status_code"] == status_code - assert exc_info.value.__cause__ is sdk_error if operation == "get-job": assert isinstance(exc_info.value, AirbyteMissingResourceError) assert exc_info.value.context["job_id"] == 42 - get_job.assert_called_once_with(api.GetJobRequest(job_id=42)) - patch_connection.assert_not_called() else: assert not isinstance(exc_info.value, AirbyteMissingResourceError) assert exc_info.value.context["connection_id"] == "connection-1" - patch_connection.assert_called_once() - get_job.assert_not_called() @pytest.mark.parametrize( @@ -1816,42 +1601,31 @@ def test_get_connector_falls_back_to_destination( source_status: int, ) -> None: """A bare destination ID must still resolve when the source lookup 404s or 403s.""" - raw_response = requests.Response() - raw_response.status_code = 200 - raw_response.url = "https://api.airbyte.com/v1/destinations/dest-id" - raw_response._content = ( - b'{"destinationId":"dest-id","name":"dest",' - b'"destinationType":"duckdb","workspaceId":"ws"}' - ) - destination_response = models.DestinationResponse( - configuration=models.DestinationDuckdb(destination_path="/tmp/test.duckdb"), - created_at=0, - definition_id="definition-id", - destination_id="dest-id", - destination_type="duckdb", - name="dest", - workspace_id="ws", - ) - airbyte_instance = SimpleNamespace( - sources=SimpleNamespace( - get_source=Mock(side_effect=_sdk_status_error(source_status)), - ), - destinations=SimpleNamespace( - get_destination=Mock( - return_value=api.GetDestinationResponse( - content_type="application/json", - status_code=200, - raw_response=raw_response, - destination_response=destination_response, + destination_wire = { + "destinationId": "dest-id", + "name": "dest", + "destinationType": "duckdb", + "definitionId": "definition-id", + "workspaceId": "ws", + "configuration": {"destination_path": "/tmp/test.duckdb"}, + "createdAt": 0, + } + calls: list[dict[str, object]] = [] + + def fake_request(**kwargs: object) -> object: + calls.append(kwargs) + if kwargs["path"].startswith("/sources/"): + raise api_util._wrap_api_error( + _http_error_response( + source_status, + url=f"https://api.airbyte.com/v1{kwargs['path']}", + text="boom", ), - ), - ), - ) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, - ) + kwargs.get("error_context"), + ) + return destination_wire + + monkeypatch.setattr(api_util, "_make_public_api_request", fake_request) connector_type, response = api_util.get_connector( "dest-id", @@ -1862,27 +1636,17 @@ def test_get_connector_falls_back_to_destination( ) assert connector_type is ConnectorType.DESTINATION - assert response is destination_response + assert response.destination_id == "dest-id" def test_get_connector_raises_missing_resource_when_neither_found( monkeypatch: pytest.MonkeyPatch, ) -> None: - airbyte_instance = SimpleNamespace( - sources=SimpleNamespace( - get_source=Mock(side_effect=_sdk_404_error("SOURCE_CONNECTION")), - ), - destinations=SimpleNamespace( - get_destination=Mock( - side_effect=_sdk_404_error("DESTINATION_CONNECTION"), - ), - ), - ) - monkeypatch.setattr( - api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, - ) + def fake_request(**kwargs: object) -> object: + _ = kwargs + raise _api_404_error() + + monkeypatch.setattr(api_util, "_make_public_api_request", fake_request) with pytest.raises(AirbyteMissingResourceError): api_util.get_connector( @@ -1921,18 +1685,24 @@ def test_get_connector_error_fallback( expected_status: int, expect_destination_call: bool, ) -> None: - get_destination = Mock( - side_effect=_sdk_status_error(destination_status or 200), - ) - airbyte_instance = SimpleNamespace( - sources=SimpleNamespace( - get_source=Mock(side_effect=_sdk_status_error(source_status)), - ), - destinations=SimpleNamespace(get_destination=get_destination), - ) - monkeypatch.setattr( - api_util, "get_airbyte_server_instance", lambda **_: airbyte_instance - ) + calls: list[str] = [] + + def fake_request(**kwargs: object) -> object: + path = kwargs["path"] + calls.append(path) + status = ( + source_status + if path.startswith("/sources/") + else (destination_status or 200) + ) + raise api_util._wrap_api_error( + _http_error_response( + status, url=f"https://api.airbyte.com/v1{path}", text="boom" + ), + kwargs.get("error_context"), + ) + + monkeypatch.setattr(api_util, "_make_public_api_request", fake_request) with pytest.raises(AirbyteCloudError) as exc_info: api_util.get_connector( @@ -1946,31 +1716,25 @@ def test_get_connector_error_fallback( context = exc_info.value.context or {} assert context["status_code"] == expected_status assert ("source_id" in context) is (expected_status == source_status) - assert get_destination.called is expect_destination_call + assert ( + any(path.startswith("/destinations/") for path in calls) + is expect_destination_call + ) -def test_get_connector_does_not_fall_back_on_non_raised_5xx_response( +def test_get_connector_does_not_fall_back_on_5xx_response( monkeypatch: pytest.MonkeyPatch, ) -> None: - """A 5xx the SDK returns instead of raising is not read as "maybe a destination".""" - get_destination = Mock() - airbyte_instance = SimpleNamespace( - sources=SimpleNamespace( - get_source=Mock( - return_value=SimpleNamespace( - status_code=500, - source_response=None, - raw_response=SimpleNamespace(text="boom", url="https://api"), - ), - ), - ), - destinations=SimpleNamespace(get_destination=get_destination), - ) - monkeypatch.setattr( - api_util, "get_airbyte_server_instance", lambda **_: airbyte_instance - ) + """A 5xx source lookup is not read as "maybe a destination".""" + calls: list[str] = [] - with pytest.raises(AirbyteMissingResourceError) as exc_info: + def fake_request(**kwargs: object) -> object: + calls.append(kwargs["path"]) + raise _api_status_error(500) + + monkeypatch.setattr(api_util, "_make_public_api_request", fake_request) + + with pytest.raises(AirbyteCloudError) as exc_info: api_util.get_connector( "connector-id", api_root="https://api.airbyte.com/v1", @@ -1980,25 +1744,16 @@ def test_get_connector_does_not_fall_back_on_non_raised_5xx_response( ) assert (exc_info.value.context or {})["status_code"] == 500 - get_destination.assert_not_called() + assert not any(path.startswith("/destinations/") for path in calls) -def test_get_source_reraises_non_404_sdk_error_as_airbyte_error( +def test_get_source_reraises_non_404_api_error_as_airbyte_error( monkeypatch: pytest.MonkeyPatch, ) -> None: - raw_response = requests.Response() - raw_response.status_code = 500 - raw_response.url = "https://api.airbyte.com/v1/sources/source-id" - error = SDKError( - "API error occurred: Status 500", 500, "response body", raw_response - ) - airbyte_instance = SimpleNamespace( - sources=SimpleNamespace(get_source=Mock(side_effect=error)), - ) monkeypatch.setattr( api_util, - "get_airbyte_server_instance", - lambda **_: airbyte_instance, + "_make_public_api_request", + Mock(side_effect=_api_status_error(500)), ) with pytest.raises(AirbyteCloudError) as exc_info: @@ -2011,3 +1766,186 @@ def test_get_source_reraises_non_404_sdk_error_as_airbyte_error( ) assert type(exc_info.value) is AirbyteCloudError + + +_FIELD_FILTERING_MAPPER = { + "type": "field-filtering", + "mapperConfiguration": {"targetField": "foo"}, +} + + +def _connection_wire_with_mapper(mapper: dict) -> dict[str, object]: + """Create a connection wire dict carrying a single stream mapper.""" + return { + "connectionId": "connection-1", + "name": "Connection", + "sourceId": "source-1", + "destinationId": "destination-1", + "workspaceId": "workspace-id", + "status": "active", + "schedule": {"scheduleType": "manual"}, + "nonBreakingSchemaUpdatesBehavior": "ignore", + "configurations": {"streams": [{"name": "s", "mappers": [mapper]}]}, + "createdAt": 0, + "tags": [], + } + + +@pytest.mark.parametrize( + "mapper", + [ + pytest.param(_FIELD_FILTERING_MAPPER, id="field-filtering"), + pytest.param( + {"type": "future-mapper", "mapperConfiguration": {"someKey": 1}}, + id="unknown-mapper-type", + ), + ], +) +def test_get_connection_decodes_stream_mappers( + monkeypatch: pytest.MonkeyPatch, + mapper: dict, +) -> None: + """Connections with field-filtering or unknown mappers decode without failing.""" + _public_api_spy(monkeypatch, [_connection_wire_with_mapper(mapper)]) + + connection = api_util.get_connection( + workspace_id="workspace-id", + connection_id="connection-1", + api_root="https://api.airbyte.com/v1", + client_id=None, + client_secret=None, + bearer_token=SecretString("token"), + ) + + stream_mapper = connection.configurations.streams[0].mappers[0] + assert stream_mapper.type == mapper["type"] + if mapper["type"] == "field-filtering": + assert stream_mapper.mapper_configuration.target_field == "foo" + else: + assert stream_mapper.mapper_configuration == {"someKey": 1} + + info = CloudConnectionInfo.from_api_response(connection) + assert info.connection_id == "connection-1" + + +@pytest.mark.parametrize( + ("status", "expected"), + [ + pytest.param("queued", JobStatusEnum.QUEUED, id="queued"), + pytest.param("some_future_status", "some_future_status", id="unknown-status"), + ], +) +def test_get_job_info_decodes_queued_and_unknown_statuses( + monkeypatch: pytest.MonkeyPatch, + status: str, + expected: object, +) -> None: + """Queued and future job statuses decode without raising.""" + _public_api_spy( + monkeypatch, + [ + { + "jobId": 42, + "status": status, + "jobType": "sync", + "startTime": "2026-01-01T00:00:00Z", + "connectionId": "connection-id", + } + ], + ) + + job = api_util.get_job_info( + job_id=42, + api_root="https://api.airbyte.com/v1", + client_id=None, + client_secret=None, + bearer_token=SecretString("token"), + ) + + assert job.status == expected + job_info = CloudJobInfo.from_api_response(job) + assert job_info.status == expected + + +def test_list_connections_skips_invalid_items_and_logs( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """A malformed item in a paged list is skipped with a warning.""" + invalid_item = {"connectionId": "connection-bad"} + _public_api_spy( + monkeypatch, + [ + _page_body( + [ + _connection_wire("good", 1), + invalid_item, + _connection_wire("also-good", 2), + ], + ), + ], + ) + + with caplog.at_level("WARNING", logger="airbyte._util.api_util"): + result = api_util.list_connections( + workspace_id="workspace-id", + api_root="https://api.airbyte.com/v1", + client_id=None, + client_secret=None, + bearer_token=SecretString("token"), + ) + + assert [connection.name for connection in result] == ["good", "also-good"] + assert any( + "failed schema validation" in record.message for record in caplog.records + ) + + +def test_cloud_connection_info_configurations_keep_snake_case_keys( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """`CloudConnectionInfo.configurations` serializes with snake_case keys (MCP output).""" + _public_api_spy( + monkeypatch, + [ + { + "connectionId": "connection-1", + "name": "Connection", + "sourceId": "source-1", + "destinationId": "destination-1", + "workspaceId": "workspace-id", + "status": "active", + "schedule": {"scheduleType": "manual"}, + "nonBreakingSchemaUpdatesBehavior": "ignore", + "configurations": { + "streams": [ + { + "name": "users", + "syncMode": "incremental_append", + "cursorField": ["updated_at"], + "primaryKey": [["id"]], + } + ] + }, + "createdAt": 0, + "tags": [], + } + ], + ) + + connection = api_util.get_connection( + workspace_id="workspace-id", + connection_id="connection-1", + api_root="https://api.airbyte.com/v1", + client_id=None, + client_secret=None, + bearer_token=SecretString("token"), + ) + + info = CloudConnectionInfo.from_api_response(connection) + dumped = info.model_dump(mode="json") + stream = dumped["configurations"]["streams"][0] + assert stream["sync_mode"] == "incremental_append" + assert stream["cursor_field"] == ["updated_at"] + assert stream["primary_key"] == [["id"]] + assert "syncMode" not in stream diff --git a/tests/unit_tests/test_cloud_client.py b/tests/unit_tests/test_cloud_client.py index 485ca0f95..a8f7fd4c7 100644 --- a/tests/unit_tests/test_cloud_client.py +++ b/tests/unit_tests/test_cloud_client.py @@ -8,7 +8,7 @@ from airbyte import exceptions as exc from airbyte.cloud.client import CloudClient from airbyte.cloud.models import WorkspacePrivilegeScope -from airbyte_api import models +from airbyte_server_models.public_api import models def _api_patches( @@ -123,7 +123,7 @@ def test_resolve_default_workspace_id_uses_exactly_one_direct_grant( patches[4], patch( "airbyte._util.api_util.get_workspace", - return_value=models.WorkspaceResponse( + return_value=models.WorkspaceResponse.model_construct( data_residency="auto", name="Workspace", notifications=models.NotificationsConfig(), @@ -153,7 +153,7 @@ def get_workspace( **kwargs: object, ) -> models.WorkspaceResponse: if workspace_id == "live-workspace": - return models.WorkspaceResponse( + return models.WorkspaceResponse.model_construct( data_residency="auto", name="Live Workspace", notifications=models.NotificationsConfig(), @@ -215,7 +215,7 @@ def test_direct_workspace_validation_is_capped() -> None: patch( "airbyte._util.api_util.get_workspace", side_effect=[ - models.WorkspaceResponse( + models.WorkspaceResponse.model_construct( data_residency="auto", name=f"Workspace {index}", notifications=models.NotificationsConfig(), @@ -283,7 +283,7 @@ def test_default_context_resolves_workspace_when_organization_lookup_fails() -> patches[4], patch( "airbyte._util.api_util.get_workspace", - return_value=models.WorkspaceResponse( + return_value=models.WorkspaceResponse.model_construct( data_residency="auto", name="Workspace 1", notifications=models.NotificationsConfig(), @@ -322,7 +322,7 @@ def test_default_context_enriches_configured_workspace() -> None: patches[4], patch( "airbyte._util.api_util.get_workspace", - return_value=models.WorkspaceResponse( + return_value=models.WorkspaceResponse.model_construct( data_residency="auto", name="Configured", notifications=models.NotificationsConfig(), @@ -464,7 +464,7 @@ def test_list_workspaces_skips_stale_grant_before_valid_grant_with_limit() -> No resource_type="workspace", resource_name_or_id="stale-workspace", ) - valid_workspace = models.WorkspaceResponse( + valid_workspace = models.WorkspaceResponse.model_construct( data_residency="auto", name="Valid workspace", notifications=models.NotificationsConfig(), @@ -528,7 +528,7 @@ def test_list_workspaces_defaults_to_direct_memberships() -> None: patches[4], patch( "airbyte._util.api_util.get_workspace", - return_value=models.WorkspaceResponse( + return_value=models.WorkspaceResponse.model_construct( data_residency="auto", name="Workspace 1", notifications=models.NotificationsConfig(), @@ -953,13 +953,13 @@ def test_list_workspaces_uses_direct_grants_when_memberships_are_ambiguous() -> patch( "airbyte._util.api_util.get_workspace", side_effect=[ - models.WorkspaceResponse( + models.WorkspaceResponse.model_construct( data_residency="auto", name="Workspace 1", notifications=models.NotificationsConfig(), workspace_id="workspace-1", ), - models.WorkspaceResponse( + models.WorkspaceResponse.model_construct( data_residency="auto", name="Workspace 2", notifications=models.NotificationsConfig(), @@ -1012,7 +1012,7 @@ def test_get_default_context_for_user_is_bounded_to_permission_derived_scope() - patch( "airbyte._util.api_util.get_workspace", side_effect=[ - models.WorkspaceResponse( + models.WorkspaceResponse.model_construct( data_residency="auto", name=f"Workspace {index}", notifications=models.NotificationsConfig(), @@ -1085,7 +1085,7 @@ def test_get_default_context_for_user_truncates_workspace_memberships() -> None: patch( "airbyte._util.api_util.get_workspace", side_effect=[ - models.WorkspaceResponse( + models.WorkspaceResponse.model_construct( data_residency="auto", name=f"Workspace {index}", notifications=models.NotificationsConfig(), @@ -1139,7 +1139,7 @@ def test_get_default_context_for_user_hints_setter_when_stored_default_missing() patches[4], patch( "airbyte._util.api_util.get_workspace", - return_value=models.WorkspaceResponse( + return_value=models.WorkspaceResponse.model_construct( data_residency="auto", name="Workspace 1", notifications=models.NotificationsConfig(), @@ -1173,7 +1173,7 @@ def test_get_default_context_for_user_omits_setter_hint_when_default_stored() -> patches[4], patch( "airbyte._util.api_util.get_workspace", - return_value=models.WorkspaceResponse( + return_value=models.WorkspaceResponse.model_construct( data_residency="auto", name="Workspace 1", notifications=models.NotificationsConfig(), diff --git a/tests/unit_tests/test_cloud_connections.py b/tests/unit_tests/test_cloud_connections.py index b27ec66cb..40506a846 100644 --- a/tests/unit_tests/test_cloud_connections.py +++ b/tests/unit_tests/test_cloud_connections.py @@ -24,8 +24,7 @@ AirbyteMissingResourceError, AirbyteLibInputError, ) -from airbyte_api import models -from airbyte_api.errors import SDKError +from airbyte_server_models.public_api import models def _job_response( @@ -35,7 +34,7 @@ def _job_response( connection_id: str = "connection-id", ) -> models.JobResponse: """Create a minimal job response.""" - return models.JobResponse( + return models.JobResponse.model_construct( connection_id=connection_id, job_id=job_id, job_type=models.JobTypeEnum.SYNC, @@ -570,7 +569,7 @@ def test_set_schedule_accepts_quartz_cron( ) -> None: """Verify Quartz cron expressions are passed through to the API.""" connection = _connection() - captured: list[models.AirbyteAPIConnectionSchedule] = [] + captured: list[models.AirbyteApiConnectionSchedule] = [] def patch_connection( *, @@ -579,11 +578,11 @@ def patch_connection( client_id: object, client_secret: object, bearer_token: object, - schedule: models.AirbyteAPIConnectionSchedule, + schedule: models.AirbyteApiConnectionSchedule, ) -> models.ConnectionResponse: _ = (connection_id, api_root, client_id, client_secret, bearer_token) captured.append(schedule) - return models.ConnectionResponse( + return models.ConnectionResponse.model_construct( connection_id="connection-id", created_at=0, destination_id="destination-id", @@ -715,21 +714,19 @@ def run_connection(**kwargs: object) -> None: fetch_mock.assert_not_called() -def test_run_connection_wraps_sdk_error( +def test_run_connection_wraps_api_error( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Verify `run_connection` wraps a create_job SDKError in AirbyteConnectionSyncError.""" - airbyte_instance = MagicMock() - airbyte_instance.jobs.create_job.side_effect = SDKError( - message="Status 409", - status_code=409, - body="...", - raw_response=MagicMock(), - ) + """Verify `run_connection` wraps a create_job API error in AirbyteConnectionSyncError.""" monkeypatch.setattr( api_util, - "get_airbyte_server_instance", - MagicMock(return_value=airbyte_instance), + "_make_public_api_request", + MagicMock( + side_effect=AirbyteCloudError( + message="API error occurred: Status 409", + context={"status_code": 409}, + ) + ), ) with pytest.raises(AirbyteConnectionSyncError) as exc_info: diff --git a/tests/unit_tests/test_cloud_credentials.py b/tests/unit_tests/test_cloud_credentials.py index 3e628f87c..c091f2cb3 100644 --- a/tests/unit_tests/test_cloud_credentials.py +++ b/tests/unit_tests/test_cloud_credentials.py @@ -9,7 +9,7 @@ import pytest import requests -from airbyte_api import models +from airbyte_server_models.public_api import models from airbyte import constants from airbyte._util import api_util @@ -158,13 +158,7 @@ def _patch_workspace_discovery( api_util, "list_permissions_for_user", lambda *_, **__: [ - _permission_response( - organization_id=( - str(permission["organizationId"]) - if permission.get("organizationId") is not None - else None - ) - ) + {"permissionId": "permission-id", "userId": "user-id", **permission} for permission in (permissions or []) ], ) @@ -564,7 +558,7 @@ def fake_create_workspace( ) -> models.WorkspaceResponse: nonlocal captured_organization_id captured_organization_id = organization_id - return models.WorkspaceResponse( + return models.WorkspaceResponse.model_construct( data_residency="auto", name="New workspace", notifications=models.NotificationsConfig(), @@ -635,7 +629,7 @@ def test_cloud_client_rename_workspace_forwards_inputs( def fake_rename_workspace(**kwargs: object) -> models.WorkspaceResponse: captured_kwargs.update(kwargs) - return models.WorkspaceResponse( + return models.WorkspaceResponse.model_construct( data_residency="auto", name="Renamed workspace", notifications=models.NotificationsConfig(), @@ -907,7 +901,7 @@ def test_cloud_workspace_rename_forwards_inputs( def fake_rename_workspace(**kwargs: object) -> models.WorkspaceResponse: captured_kwargs.update(kwargs) - return models.WorkspaceResponse( + return models.WorkspaceResponse.model_construct( data_residency="auto", name="Renamed workspace", notifications=models.NotificationsConfig(), @@ -1006,7 +1000,7 @@ def test_cloud_client_get_organization_uses_default_organization_id( api_util, "list_organizations_for_user", lambda **_: [ - models.OrganizationResponse( + models.OrganizationResponse.model_construct( organization_id="default-org", organization_name="Default Org", email="test@example.com", @@ -1073,13 +1067,7 @@ def test_cloud_client_get_organization_resolves_default_context( api_util, "list_permissions_for_user", lambda *_, **__: [ - _permission_response( - organization_id=( - str(permission["organizationId"]) - if permission.get("organizationId") is not None - else None - ) - ) + {"permissionId": "permission-id", "userId": "user-id", **permission} for permission in (permissions or []) ], ) @@ -1209,7 +1197,7 @@ def fake_get_organization_info(**kwargs: object) -> OrganizationInfoRead: pytest.param([], id="empty"), pytest.param( [ - models.OrganizationResponse( + models.OrganizationResponse.model_construct( organization_id="organization-id", organization_name="Organization", email="test@example.com", @@ -1219,12 +1207,12 @@ def fake_get_organization_info(**kwargs: object) -> OrganizationInfoRead: ), pytest.param( [ - models.OrganizationResponse( + models.OrganizationResponse.model_construct( organization_id="organization-id-1", organization_name="Organization 1", email="one@example.com", ), - models.OrganizationResponse( + models.OrganizationResponse.model_construct( organization_id="organization-id-2", organization_name="Organization 2", email="two@example.com", @@ -1266,17 +1254,17 @@ def test_cloud_client_list_organizations_uses_config_api_for_filter_or_limit( ) -> None: captured: dict[str, object] = {} organizations = [ - models.OrganizationResponse( + models.OrganizationResponse.model_construct( organization_id="organization-id-1", organization_name="Development", email="one@example.com", ), - models.OrganizationResponse( + models.OrganizationResponse.model_construct( organization_id="organization-id-2", organization_name="development-copy", email="two@example.com", ), - models.OrganizationResponse( + models.OrganizationResponse.model_construct( organization_id="organization-id-3", organization_name="Production", email="three@example.com", @@ -1331,7 +1319,7 @@ def test_cloud_client_list_organizations_falls_back_to_public_listing( monkeypatch: pytest.MonkeyPatch, ) -> None: organizations = [ - models.OrganizationResponse( + models.OrganizationResponse.model_construct( organization_id="organization-id", organization_name="Development", email="test@example.com", @@ -1459,7 +1447,7 @@ def test_cloud_client_list_organizations_has_no_default_limit( monkeypatch: pytest.MonkeyPatch, ) -> None: organizations = [ - models.OrganizationResponse( + models.OrganizationResponse.model_construct( organization_id=f"organization-id-{index}", organization_name=f"Organization {index}", email=f"test-{index}@example.com", @@ -1678,11 +1666,7 @@ def fake_list_permissions_for_user( ) -> list[PermissionRead]: calls["permissions"] += 1 return [ - _permission_response( - organization_id=str(permission["organizationId"]) - if permission.get("organizationId") is not None - else None - ) + {"permissionId": "permission-id", "userId": "user-id", **permission} for permission in permissions ] @@ -1698,46 +1682,6 @@ def fake_list_permissions_for_user( assert calls == {"user": 1, "permissions": 1} -def test_cloud_client_list_workspaces_rejects_ambiguous_memberships_with_candidates( - monkeypatch: pytest.MonkeyPatch, -) -> None: - _patch_workspace_discovery( - monkeypatch, - permissions=[ - { - "permissionType": "organization_member", - "organizationId": "organization-1", - }, - { - "permissionType": "organization_member", - "organizationId": "organization-2", - }, - ], - ) - - def fake_get_organization_info(**kwargs: object) -> OrganizationInfoRead: - if kwargs["organization_id"] == "organization-2": - raise AirbyteCloudError(message="Forbidden") - return _organization_info_response(organization_name="Organization 1") - - monkeypatch.setattr(api_util, "get_organization_info", fake_get_organization_info) - - with pytest.raises(AirbyteLibInputError) as exc_info: - CloudClient(bearer_token="token").list_workspaces() - - assert exc_info.value.context == { - "organization_ids": ["organization-1", "organization-2"], - "organization_candidates": [ - { - "organization_id": "organization-1", - "organization_name": "Organization 1", - }, - {"organization_id": "organization-2", "organization_name": None}, - ], - "total_candidates": 2, - } - - @pytest.mark.parametrize( ("permissions", "privilege_scope"), [ diff --git a/tests/unit_tests/test_cloud_models.py b/tests/unit_tests/test_cloud_models.py index 9825a9acd..1bfaab60f 100644 --- a/tests/unit_tests/test_cloud_models.py +++ b/tests/unit_tests/test_cloud_models.py @@ -15,10 +15,8 @@ ConnectionSchedule, ConnectionStatus, ) -from airbyte_api.models import ( - DestinationDuckdb, +from airbyte_server_models.public_api.models import ( DestinationResponse, - SourceFaker, SourceResponse, ) @@ -28,7 +26,7 @@ [ pytest.param( SourceResponse( - configuration=SourceFaker(), + configuration={}, created_at=1, definition_id="source-faker-definition", name="Test source", @@ -42,7 +40,7 @@ id="source", ), pytest.param( - SourceResponse( + SourceResponse.model_construct( configuration=None, created_at=1, definition_id="source-empty-definition", @@ -58,7 +56,7 @@ ), pytest.param( DestinationResponse( - configuration=DestinationDuckdb(destination_path="/tmp/test.duckdb"), + configuration={"destination_path": "/tmp/test.duckdb"}, created_at=1, definition_id="destination-duckdb-definition", destination_id="destination-id", @@ -193,7 +191,7 @@ def test_connection_schedule_friendly_description( pytest.param( CloudSource._from_source_response, SourceResponse( - configuration=SourceFaker(), + configuration={}, created_at=1, definition_id="source-faker-definition", name="Test source", @@ -207,7 +205,7 @@ def test_connection_schedule_friendly_description( pytest.param( CloudDestination._from_destination_response, DestinationResponse( - configuration=DestinationDuckdb(destination_path="/tmp/test.duckdb"), + configuration={"destination_path": "/tmp/test.duckdb"}, created_at=1, definition_id="destination-duckdb-definition", destination_id="destination-id", diff --git a/tests/unit_tests/test_public_api_imports.py b/tests/unit_tests/test_public_api_imports.py index 89af07d32..945f248b0 100644 --- a/tests/unit_tests/test_public_api_imports.py +++ b/tests/unit_tests/test_public_api_imports.py @@ -11,9 +11,18 @@ REPO_ROOT = Path(__file__).parents[2] PUBLIC_MODULE_RESTRICTED_IMPORTS = { - Path("airbyte/cli"): ("airbyte_api", "airbyte._util.api_imports"), - Path("airbyte/mcp"): ("airbyte_api", "airbyte._util.api_imports"), - Path("airbyte/cloud"): ("airbyte_api", "airbyte._util.api_imports"), + Path("airbyte/cli"): ( + "airbyte_server_models.public_api", + "airbyte._util.api_imports", + ), + Path("airbyte/mcp"): ( + "airbyte_server_models.public_api", + "airbyte._util.api_imports", + ), + Path("airbyte/cloud"): ( + "airbyte_server_models.public_api", + "airbyte._util.api_imports", + ), } PUBLIC_MODULE_RESTRICTED_REFERENCES = { Path("airbyte/cli"): ("api_util.models",), diff --git a/uv.lock b/uv.lock index 46106d3b2..6918ec0a4 100644 --- a/uv.lock +++ b/uv.lock @@ -107,7 +107,6 @@ wheels = [ name = "airbyte" source = { editable = "." } dependencies = [ - { name = "airbyte-api" }, { name = "airbyte-cdk" }, { name = "airbyte-protocol-models-pdv2" }, { name = "airbyte-server-models" }, @@ -187,10 +186,9 @@ dev = [ [package.metadata] requires-dist = [ - { name = "airbyte-api", specifier = ">=0.53.0,<1.0" }, { name = "airbyte-cdk", specifier = ">=7.28.3,<8.0.0" }, { name = "airbyte-protocol-models-pdv2", specifier = ">=0.13.0,<1.0" }, - { name = "airbyte-server-models", git = "https://github.com/airbytehq/airbyte-ops-mcp.git?subdirectory=airbyte-server-models&branch=devin%2F1788977312-server-models" }, + { name = "airbyte-server-models", git = "https://github.com/airbytehq/airbyte-ops-mcp.git?subdirectory=airbyte-server-models&rev=9a6e0ec03a96d3ac4d7bd83550ddad19da3fe96b" }, { name = "cryptography", specifier = ">=44.0.0,!=45.0.0,!=45.0.1,<47.0.0" }, { name = "cyclopts", specifier = ">=4.0,<5.0" }, { name = "ddtrace", marker = "extra == 'datadog'", specifier = ">=4.15.2,<5.0" }, @@ -262,31 +260,6 @@ dev = [ { name = "viztracer", specifier = ">=0.16.3,<1.1.0" }, ] -[[package]] -name = "airbyte-api" -version = "0.53.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "dataclasses-json" }, - { name = "idna" }, - { name = "jsonpath-python" }, - { name = "marshmallow" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "python-dateutil" }, - { name = "requests" }, - { name = "six" }, - { name = "typing-extensions" }, - { name = "typing-inspect" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ad/23/8debf9c4ca8652c9ceb60b8074e654191ba2053001616151993b39d6cdcb/airbyte-api-0.53.0.tar.gz", hash = "sha256:f054ed170f9a691c3304e93a5212670fd2a38e5debb667c72d5ef8eb89cf7e9d", size = 330090, upload-time = "2025-10-02T19:55:43.378Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/f5/ca44b6f3919f049a4441755958c042fcea09a44e876352a6005f5e5a9e46/airbyte_api-0.53.0-py3-none-any.whl", hash = "sha256:0bd86d5122789a7c97115a4b52ce492b013d0ea8eeab597b6495b0b310332f28", size = 773825, upload-time = "2025-10-02T19:55:42.043Z" }, -] - [[package]] name = "airbyte-cdk" version = "7.28.3" @@ -362,8 +335,8 @@ wheels = [ [[package]] name = "airbyte-server-models" -version = "0.0.0.post792.dev0+85882b5" -source = { git = "https://github.com/airbytehq/airbyte-ops-mcp.git?subdirectory=airbyte-server-models&branch=devin%2F1788977312-server-models#85882b5e45fdb2b6b0ca5c5844f4f5e1ab8c9a44" } +version = "0.0.0.post868.dev0+9a6e0ec" +source = { git = "https://github.com/airbytehq/airbyte-ops-mcp.git?subdirectory=airbyte-server-models&rev=9a6e0ec03a96d3ac4d7bd83550ddad19da3fe96b#9a6e0ec03a96d3ac4d7bd83550ddad19da3fe96b" } dependencies = [ { name = "pydantic", extra = ["email"] }, ] @@ -802,19 +775,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/a3/2e00fececc34a99ae3a5d5702a5dd29c5371e4ed016647301a2b9bcc1976/cyclopts-4.5.0-py3-none-any.whl", hash = "sha256:305b9aa90a9cd0916f0a450b43e50ad5df9c252680731a0719edfb9b20381bf5", size = 199772, upload-time = "2026-01-16T02:07:14.707Z" }, ] -[[package]] -name = "dataclasses-json" -version = "0.6.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "marshmallow" }, - { name = "typing-inspect" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, -] - [[package]] name = "dateparser" version = "1.2.2" @@ -1896,15 +1856,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/6a/71937d4760bf6f3beabc640cf18578683b5247845db5ee4dbd8c66898def/joserfc-1.5.0-py3-none-any.whl", hash = "sha256:eaaded4f4c6717a761baa41b4067307d0c246b9d5e38acd44e80a332f5ddaf24", size = 68388, upload-time = "2025-11-30T06:01:50.43Z" }, ] -[[package]] -name = "jsonpath-python" -version = "1.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/bf/626a72f2d093c5eb4f4de55b443714afa7231beeae40d4a1c69b5c5aa4d1/jsonpath_python-1.1.4.tar.gz", hash = "sha256:bb3e13854e4807c078a1503ae2d87c211b8bff4d9b40b6455ed583b3b50a7fdd", size = 84766, upload-time = "2025-11-25T12:08:39.521Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/bc/52e5bf0d9839e082b976c19afcab7561d0d719c7627483bf5dc251d27eed/jsonpath_python-1.1.4-py3-none-any.whl", hash = "sha256:8700cb8610c44da6e5e9bff50232779c44bf7dc5bc62662d49319ee746898442", size = 12687, upload-time = "2025-11-25T12:08:38.453Z" }, -] - [[package]] name = "jsonref" version = "1.1.0" @@ -2057,18 +2008,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, ] -[[package]] -name = "marshmallow" -version = "3.26.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, -] - [[package]] name = "mcp" version = "2.2.0" @@ -2212,15 +2151,6 @@ wheels = [ { 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-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 = "nexus-rpc" version = "1.2.0" @@ -4057,19 +3987,6 @@ 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-inspect" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, -] - [[package]] name = "typing-inspection" version = "0.4.2" From 40f718a2d3e501565cdf5b4ae054c9c6b6d47fef Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:48:15 +0000 Subject: [PATCH 08/11] fix: paginate lists by raw page size; wire private-git-deps auth into lint/pytest CI Co-Authored-By: AJ Steers --- .../actions/private-git-deps-auth/action.yml | 2 +- .github/workflows/python_lint.yml | 20 +++++++++ .github/workflows/python_pytest.yml | 25 +++++++++++ airbyte/_util/api_util.py | 43 ++++++++++-------- airbyte/mcp/_sso_auth.py | 2 +- airbyte/mcp/_user_identity.py | 18 ++++++-- pyproject.toml | 2 +- tests/unit_tests/test_cloud_api_util.py | 45 +++++++++++++------ tests/unit_tests/test_mcp_airbyte_user_id.py | 6 ++- 9 files changed, 123 insertions(+), 40 deletions(-) diff --git a/.github/actions/private-git-deps-auth/action.yml b/.github/actions/private-git-deps-auth/action.yml index 0b3fa5ab1..262534435 100644 --- a/.github/actions/private-git-deps-auth/action.yml +++ b/.github/actions/private-git-deps-auth/action.yml @@ -1,4 +1,4 @@ -# TK-TODO: Temporary. `airbyte-server-models` is consumed from the private airbyte-ops-mcp repo +# TK-TODO: Temporary. `airbyte-server-models` is consumed from the private airbyte-ops-mcp repo # IGNORE:TK # until it is published to PyPI (airbytehq/airbyte-ops-mcp#1326). Remove this action and its # call sites once pyproject.toml pins a released version. name: Authenticate git for private dependency repos diff --git a/.github/workflows/python_lint.yml b/.github/workflows/python_lint.yml index 8cddeeebe..f62280b92 100644 --- a/.github/workflows/python_lint.yml +++ b/.github/workflows/python_lint.yml @@ -24,6 +24,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -44,6 +49,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -64,6 +74,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -86,6 +101,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev diff --git a/.github/workflows/python_pytest.yml b/.github/workflows/python_pytest.yml index c74862a48..2a5a1d6ad 100644 --- a/.github/workflows/python_pytest.yml +++ b/.github/workflows/python_pytest.yml @@ -42,6 +42,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -130,6 +135,11 @@ jobs: with: version: "latest" python-version: ${{ matrix.python-version }} + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -233,6 +243,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -311,6 +326,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev @@ -357,6 +377,11 @@ jobs: with: version: "latest" python-version: '3.11' + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} - name: Install dev dependencies # We only need helper libraries run: uv sync --group dev diff --git a/airbyte/_util/api_util.py b/airbyte/_util/api_util.py index 382ff71f2..9c5edbb92 100644 --- a/airbyte/_util/api_util.py +++ b/airbyte/_util/api_util.py @@ -671,12 +671,13 @@ def list_connections( bearer_token=bearer_token, error_context={"workspace_id": workspace_id, "api_root": api_root}, ) + raw_items = (body or {}).get("data") or [] + if not raw_items: + break + page_data = _decode_list_items( body, item_model=models.ConnectionResponse, list_path="/connections" ) - if not page_data: - break - matching_connections = [ connection for connection in page_data if name_filter(connection.name) ] @@ -690,7 +691,7 @@ def list_connections( if not (body or {}).get("next"): break - current_offset += len(page_data) + current_offset += len(raw_items) return result @@ -742,12 +743,13 @@ def list_workspaces( bearer_token=bearer_token, error_context={"workspace_id": workspace_id, "api_root": api_root}, ) + raw_items = (body or {}).get("data") or [] + if not raw_items: + break + page_data = _decode_list_items( body, item_model=models.WorkspaceResponse, list_path="/workspaces" ) - if not page_data: - break - matching_workspaces = [workspace for workspace in page_data if name_filter(workspace.name)] page_results = matching_workspaces if remaining is None else matching_workspaces[:remaining] result += page_results @@ -757,7 +759,7 @@ def list_workspaces( if not (body or {}).get("next"): break - current_offset += len(page_data) + current_offset += len(raw_items) return result @@ -798,10 +800,11 @@ def list_sources( bearer_token=bearer_token, error_context={"workspace_id": workspace_id, "api_root": api_root}, ) - page_data = _decode_list_items(body, item_model=models.SourceResponse, list_path="/sources") - if not page_data: + raw_items = (body or {}).get("data") or [] + if not raw_items: break + page_data = _decode_list_items(body, item_model=models.SourceResponse, list_path="/sources") matching_sources = [source for source in page_data if name_filter(source.name)] page_results = matching_sources if remaining is None else matching_sources[:remaining] result += page_results @@ -811,7 +814,7 @@ def list_sources( if not (body or {}).get("next"): break - current_offset += len(page_data) + current_offset += len(raw_items) return result @@ -852,12 +855,13 @@ def list_destinations( bearer_token=bearer_token, error_context={"workspace_id": workspace_id, "api_root": api_root}, ) + raw_items = (body or {}).get("data") or [] + if not raw_items: + break + page_data = _decode_list_items( body, item_model=models.DestinationResponse, list_path="/destinations" ) - if not page_data: - break - matching_destinations = [ destination for destination in page_data if name_filter(destination.name) ] @@ -871,7 +875,7 @@ def list_destinations( if not (body or {}).get("next"): break - current_offset += len(page_data) + current_offset += len(raw_items) return result @@ -1044,16 +1048,17 @@ def get_job_logs( # noqa: PLR0913 # Too many arguments - needed for auth flexi error_context=base_context, ) - page_data = _decode_list_items(body, item_model=models.JobResponse, list_path="/jobs") - if not page_data: + raw_items = (body or {}).get("data") or [] + if not raw_items: break + page_data = _decode_list_items(body, item_model=models.JobResponse, list_path="/jobs") result += page_data if remaining is not None: remaining -= len(page_data) - current_offset += len(page_data) + current_offset += len(raw_items) - if not (body or {}).get("next") or len(page_data) < page_limit: + if not (body or {}).get("next") or len(raw_items) < page_limit: break return result if limit is None else result[:limit] diff --git a/airbyte/mcp/_sso_auth.py b/airbyte/mcp/_sso_auth.py index 615694a2f..55193b864 100644 --- a/airbyte/mcp/_sso_auth.py +++ b/airbyte/mcp/_sso_auth.py @@ -169,7 +169,7 @@ def _token_has_airbyte_user(access_token: str) -> bool: return False raise - user_id = user.get("userId") + user_id = user.get("userId") if isinstance(user, dict) else str(user.userId or "") return isinstance(user_id, str) and bool(user_id) diff --git a/airbyte/mcp/_user_identity.py b/airbyte/mcp/_user_identity.py index a6c13040e..4ffb035a4 100644 --- a/airbyte/mcp/_user_identity.py +++ b/airbyte/mcp/_user_identity.py @@ -155,6 +155,18 @@ def airbyte_user_properties() -> dict[str, Any]: return {"airbyte_user_id": current_airbyte_user_id()} +def _record_string(record: Any, key: str) -> str | None: # noqa: ANN401 + """Read a string field from a Config API response, whether a model or a raw dict. + + Model fields may be non-`str` types (e.g. `UUID`); coerce those to `str` so + callers keep the string semantics the raw-JSON path had. + """ + value = record.get(key) if isinstance(record, dict) else getattr(record, key, None) + if isinstance(value, str): + return value + return str(value) if value is not None else None + + def _lookup_airbyte_user( auth_user_id: str, *, @@ -171,10 +183,10 @@ def _lookup_airbyte_user( bearer_token=bearer_token, timeout=(USER_ID_LOOKUP_TIMEOUT_SECONDS, USER_ID_LOOKUP_TIMEOUT_SECONDS), ) - user_id = user.get("userId") + user_id = _record_string(user, "userId") if not isinstance(user_id, str) or not user_id: return None - default_workspace_id = user.get("defaultWorkspaceId") + default_workspace_id = _record_string(user, "defaultWorkspaceId") return AirbyteUser( user_id=user_id, default_workspace_id=( @@ -276,7 +288,7 @@ def _lookup_workspace_organization_id( bearer_token=bearer_token, timeout=(USER_ID_LOOKUP_TIMEOUT_SECONDS, USER_ID_LOOKUP_TIMEOUT_SECONDS), ) - organization_id = organization.get("organizationId") + organization_id = _record_string(organization, "organizationId") return organization_id if isinstance(organization_id, str) and organization_id else None diff --git a/pyproject.toml b/pyproject.toml index 3b3df2353..2ed965734 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,7 +115,7 @@ fallback-version = "0.0.0" allow-direct-references = true [tool.uv.sources] -# TK-TODO: `airbyte-server-models` is consumed from a git branch while the package is +# TK-TODO: `airbyte-server-models` is consumed from a git branch while the package is # IGNORE:TK # unreleased. Pin this to a published PyPI version (and remove this source entry) # before the next PyAirbyte release. # CI authenticates to the private repo via .github/actions/private-git-deps-auth (Octavia bot app). diff --git a/tests/unit_tests/test_cloud_api_util.py b/tests/unit_tests/test_cloud_api_util.py index 9459de2d0..db02ca2ae 100644 --- a/tests/unit_tests/test_cloud_api_util.py +++ b/tests/unit_tests/test_cloud_api_util.py @@ -1869,37 +1869,54 @@ def test_get_job_info_decodes_queued_and_unknown_statuses( def test_list_connections_skips_invalid_items_and_logs( monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, ) -> None: - """A malformed item in a paged list is skipped with a warning.""" + """A malformed item in a paged list is skipped with a warning. + + Pagination must advance by the RAW item count, not the decoded count: + a skipped item must not shift the next page's offset (which would yield + duplicates), and a page of only invalid items must not stop pagination + while `next` is still set. + """ invalid_item = {"connectionId": "connection-bad"} - _public_api_spy( + invalid_only = {"connectionId": "connection-bad-2"} + captured = _public_api_spy( monkeypatch, [ + # Page 1: one invalid item among three raw items. _page_body( [ _connection_wire("good", 1), invalid_item, _connection_wire("also-good", 2), ], + next_page="next", ), + # Page 2: every item fails to decode, but `next` is still set. + _page_body([invalid_only], next_page="next"), + _page_body([_connection_wire("third", 3)]), ], ) - with caplog.at_level("WARNING", logger="airbyte._util.api_util"): - result = api_util.list_connections( - workspace_id="workspace-id", - api_root="https://api.airbyte.com/v1", - client_id=None, - client_secret=None, - bearer_token=SecretString("token"), - ) + warnings: list[str] = [] - assert [connection.name for connection in result] == ["good", "also-good"] - assert any( - "failed schema validation" in record.message for record in caplog.records + def record_warning(message: str, *args: object, **kwargs: object) -> None: + warnings.append(message % args if args else message) + + monkeypatch.setattr(api_util.logger, "warning", record_warning) + + result = api_util.list_connections( + workspace_id="workspace-id", + api_root="https://api.airbyte.com/v1", + client_id=None, + client_secret=None, + bearer_token=SecretString("token"), ) + # No duplicates: offset advanced by the raw page lengths (3, then 1). + assert [connection.name for connection in result] == ["good", "also-good", "third"] + assert [call["params"]["offset"] for call in captured] == [0, 3, 4] + assert any("failed schema validation" in message for message in warnings) + def test_cloud_connection_info_configurations_keep_snake_case_keys( monkeypatch: pytest.MonkeyPatch, diff --git a/tests/unit_tests/test_mcp_airbyte_user_id.py b/tests/unit_tests/test_mcp_airbyte_user_id.py index e1444d983..430bc6fc5 100644 --- a/tests/unit_tests/test_mcp_airbyte_user_id.py +++ b/tests/unit_tests/test_mcp_airbyte_user_id.py @@ -268,7 +268,11 @@ def test_lookup_request_has_a_finite_http_timeout( """A hung Config API can't pin the lookup's worker thread indefinitely.""" request = MagicMock() request.return_value.status_code = 200 - request.return_value.json.return_value = {"userId": USER_A} + request.return_value.json.return_value = { + "userId": USER_A, + "email": "user-a@example.com", + "metadata": {}, + } monkeypatch.setattr(api_util.requests, "request", request) _call("keycloak-a") From 6773948b8690b47ffa14c319b7287dd0d46bfc2a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:51:34 +0000 Subject: [PATCH 09/11] revert: restore TK-TODO merge gate for private git dependency Co-Authored-By: AJ Steers --- .github/actions/private-git-deps-auth/action.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/private-git-deps-auth/action.yml b/.github/actions/private-git-deps-auth/action.yml index 262534435..0b3fa5ab1 100644 --- a/.github/actions/private-git-deps-auth/action.yml +++ b/.github/actions/private-git-deps-auth/action.yml @@ -1,4 +1,4 @@ -# TK-TODO: Temporary. `airbyte-server-models` is consumed from the private airbyte-ops-mcp repo # IGNORE:TK +# TK-TODO: Temporary. `airbyte-server-models` is consumed from the private airbyte-ops-mcp repo # until it is published to PyPI (airbytehq/airbyte-ops-mcp#1326). Remove this action and its # call sites once pyproject.toml pins a released version. name: Authenticate git for private dependency repos diff --git a/pyproject.toml b/pyproject.toml index 2ed965734..3b3df2353 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,7 +115,7 @@ fallback-version = "0.0.0" allow-direct-references = true [tool.uv.sources] -# TK-TODO: `airbyte-server-models` is consumed from a git branch while the package is # IGNORE:TK +# TK-TODO: `airbyte-server-models` is consumed from a git branch while the package is # unreleased. Pin this to a published PyPI version (and remove this source entry) # before the next PyAirbyte release. # CI authenticates to the private repo via .github/actions/private-git-deps-auth (Octavia bot app). From 30edcb45b5d50f533a2353a4b2565d255e43ca37 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:57:14 +0000 Subject: [PATCH 10/11] fix: guard none organizationId and paymentStatus; auth uv-lock job for private deps; least-privilege app token Co-Authored-By: AJ Steers --- .../actions/private-git-deps-auth/action.yml | 1 + .github/workflows/uv-lock-command.yml | 6 +++++ airbyte/cloud/client.py | 6 ++--- airbyte/cloud/organizations.py | 6 ++++- tests/unit_tests/test_cloud_client.py | 16 ++++++++++++++ tests/unit_tests/test_cloud_credentials.py | 18 +++++++++++++++ .../test_deferred_cloud_credentials.py | 22 +++++++++++++++++-- 7 files changed, 69 insertions(+), 6 deletions(-) diff --git a/.github/actions/private-git-deps-auth/action.yml b/.github/actions/private-git-deps-auth/action.yml index 0b3fa5ab1..3d026979f 100644 --- a/.github/actions/private-git-deps-auth/action.yml +++ b/.github/actions/private-git-deps-auth/action.yml @@ -19,6 +19,7 @@ runs: with: owner: "airbytehq" repositories: "airbyte-ops-mcp" + permission-contents: read app-id: ${{ inputs.app-id }} private-key: ${{ inputs.private-key }} - name: Configure git credentials for airbyte-ops-mcp diff --git a/.github/workflows/uv-lock-command.yml b/.github/workflows/uv-lock-command.yml index 0f09317d9..f41b2ee22 100644 --- a/.github/workflows/uv-lock-command.yml +++ b/.github/workflows/uv-lock-command.yml @@ -92,6 +92,12 @@ jobs: # Run `uv lock` + - name: Authenticate git for private dependencies + uses: ./.github/actions/private-git-deps-auth + with: + app-id: ${{ secrets.OCTAVIA_BOT_APP_ID }} + private-key: ${{ secrets.OCTAVIA_BOT_PRIVATE_KEY }} + - name: Run `uv lock` run: uv lock diff --git a/airbyte/cloud/client.py b/airbyte/cloud/client.py index d90c68fd5..7e3ac4570 100644 --- a/airbyte/cloud/client.py +++ b/airbyte/cloud/client.py @@ -635,9 +635,9 @@ def _get_workspace_parent_organization_id(self, workspace_id: str) -> str: bearer_token=self._get_config_api_bearer_token(), ) organization_record = _api_record_to_dict(organization) - resolved_organization_id = str(organization_record.get("organizationId")) - if resolved_organization_id: - return resolved_organization_id + organization_id_value = organization_record.get("organizationId") + if organization_id_value: + return str(organization_id_value) raise exc.AirbyteCloudError( message="The workspace response did not include an organization ID.", context={"workspace_id": workspace_id, "response": organization}, diff --git a/airbyte/cloud/organizations.py b/airbyte/cloud/organizations.py index c9f8aecff..30851d4a0 100644 --- a/airbyte/cloud/organizations.py +++ b/airbyte/cloud/organizations.py @@ -150,7 +150,11 @@ def payment_status(self) -> str | None: """Payment status of the organization.""" info = self._fetch_organization_info() billing = info.billing if info is not None else None - return billing.paymentStatus.value if billing is not None else None + return ( + billing.paymentStatus.value + if billing is not None and billing.paymentStatus is not None + else None + ) @property def subscription_status(self) -> str | None: diff --git a/tests/unit_tests/test_cloud_client.py b/tests/unit_tests/test_cloud_client.py index a8f7fd4c7..2f7922890 100644 --- a/tests/unit_tests/test_cloud_client.py +++ b/tests/unit_tests/test_cloud_client.py @@ -252,6 +252,22 @@ def test_direct_workspace_validation_is_capped() -> None: assert get_workspace_organization_info.call_count == 25 +def test_workspace_parent_organization_id_missing_returns_none() -> None: + """An organization record without `organizationId` resolves to None, not 'None'.""" + patches = _api_patches(user={"userId": "user-id"}) + with ( + patches[0], + patches[1], + patches[2], + patches[3] as get_workspace_organization_info, + patches[4], + ): + get_workspace_organization_info.return_value = {"organizationName": "Org"} + client = CloudClient(bearer_token="token") + + assert client.get_workspace_parent_organization_id("ws-1") is None + + def test_workspace_organization_failure_is_cached() -> None: patches = _api_patches(user={"userId": "user-id"}) with ( diff --git a/tests/unit_tests/test_cloud_credentials.py b/tests/unit_tests/test_cloud_credentials.py index c091f2cb3..a63ff6e20 100644 --- a/tests/unit_tests/test_cloud_credentials.py +++ b/tests/unit_tests/test_cloud_credentials.py @@ -3209,6 +3209,24 @@ def test_cloud_organization_get_billing_status_requires_billing( organization.get_billing_status() +def test_cloud_organization_payment_status_none_when_billing_status_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A missing paymentStatus yields None instead of an AttributeError.""" + monkeypatch.setattr( + api_util, + "get_organization_info", + lambda **_: SimpleNamespace( + billing=SimpleNamespace( + paymentStatus=None, + subscriptionStatus="subscribed", + ) + ), + ) + organization = CloudOrganization(organization_id="organization-id") + assert organization.payment_status is None + + def test_cloud_organization_get_billing_status_wraps_transport_error( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/tests/unit_tests/test_deferred_cloud_credentials.py b/tests/unit_tests/test_deferred_cloud_credentials.py index fc38e9c31..eecc4051e 100644 --- a/tests/unit_tests/test_deferred_cloud_credentials.py +++ b/tests/unit_tests/test_deferred_cloud_credentials.py @@ -522,12 +522,30 @@ def test_get_connector_metadata_by_definition_id( ) +_CHECK_CONNECTION_JOB_INFO = { + "id": "00000000-0000-0000-0000-000000000001", + "configType": "check_connection_source", + "status": "succeeded", + "createdAt": 0, + "endedAt": 1, + "succeeded": True, +} + + @responses.activate @pytest.mark.parametrize( ("status_code", "body", "expected"), [ - (200, {"status": "succeeded"}, True), - (200, {"status": "failed", "message": "Invalid credential abc123"}, False), + (200, {"status": "succeeded", "jobInfo": _CHECK_CONNECTION_JOB_INFO}, True), + ( + 200, + { + "status": "failed", + "message": "Invalid credential abc123", + "jobInfo": _CHECK_CONNECTION_JOB_INFO, + }, + False, + ), (422, {"message": "Missing required settings abc123"}, False), (500, {"message": "Unexpected failure abc123"}, None), ], From 0a28f347ee5485c85e04ada31b7bcb82ca4b9e29 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 03:17:35 +0000 Subject: [PATCH 11/11] fix: send connector configuration verbatim; decode declarative manifest to dict; bump airbyte-server-models Co-Authored-By: AJ Steers --- airbyte/_util/api_util.py | 16 +++-- airbyte/cloud/models.py | 6 +- pyproject.toml | 2 +- .../cloud/test_cloud_api_util.py | 6 +- tests/unit_tests/test_cloud_api_util.py | 59 +++++++++++++++++++ uv.lock | 6 +- 6 files changed, 83 insertions(+), 12 deletions(-) diff --git a/airbyte/_util/api_util.py b/airbyte/_util/api_util.py index 9c5edbb92..bc09a7501 100644 --- a/airbyte/_util/api_util.py +++ b/airbyte/_util/api_util.py @@ -308,6 +308,16 @@ def _api_headers( } +def _serialize_public_api_request(request: BaseModel) -> dict[str, Any]: + body = request.model_dump(mode="json", by_alias=True, exclude_none=True) + # Free-form connector configuration is sent verbatim (nulls included): + # exclude_none only applies to the request's own top-level fields. + configuration = getattr(request, "configuration", None) + if isinstance(configuration, BaseModel): + body["configuration"] = configuration.model_dump(mode="json", by_alias=True) + return body + + @overload def _make_public_api_request( # Mirrors the API surface. *, @@ -371,11 +381,7 @@ def _make_public_api_request( # noqa: PLR0913 # Mirrors the API surface. url=full_url, headers=headers, params=params, - json=( - request.model_dump(mode="json", by_alias=True, exclude_none=True) - if request is not None - else None - ), + json=(_serialize_public_api_request(request) if request is not None else None), # No timeout, matching the former SDK behavior. ) if not status_ok(response.status_code): diff --git a/airbyte/cloud/models.py b/airbyte/cloud/models.py index 1d697c6c6..454425b1b 100644 --- a/airbyte/cloud/models.py +++ b/airbyte/cloud/models.py @@ -580,7 +580,11 @@ def from_api_response( return cls( definition_id=definition.id, name=definition.name, - manifest=definition.manifest, + manifest=( + definition.manifest.model_dump(mode="json", by_alias=True) + if isinstance(definition.manifest, BaseModel) + else definition.manifest + ), version=str(definition.version), ) diff --git a/pyproject.toml b/pyproject.toml index 3b3df2353..a2914f1b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,7 +119,7 @@ allow-direct-references = true # unreleased. Pin this to a published PyPI version (and remove this source entry) # before the next PyAirbyte release. # CI authenticates to the private repo via .github/actions/private-git-deps-auth (Octavia bot app). -airbyte-server-models = { git = "https://github.com/airbytehq/airbyte-ops-mcp.git", rev = "9a6e0ec03a96d3ac4d7bd83550ddad19da3fe96b", subdirectory = "airbyte-server-models" } +airbyte-server-models = { git = "https://github.com/airbytehq/airbyte-ops-mcp.git", rev = "b314c99aaa0ddc0e53ce79f94c1d82d26f94a5f0", subdirectory = "airbyte-server-models" } [tool.hatch.build.targets.wheel] packages = ["airbyte"] diff --git a/tests/integration_tests/cloud/test_cloud_api_util.py b/tests/integration_tests/cloud/test_cloud_api_util.py index 62af50b55..e700cc8f7 100644 --- a/tests/integration_tests/cloud/test_cloud_api_util.py +++ b/tests/integration_tests/cloud/test_cloud_api_util.py @@ -110,7 +110,7 @@ def test_create_and_delete_source( airbyte_cloud_client_secret: SecretString, ) -> None: new_resource_name = "deleteme-source-faker" + text_util.generate_random_suffix() - source_config = {"seed": None} + source_config = {"sourceType": "faker"} source = api_util.create_source( name=new_resource_name, workspace_id=workspace_id, @@ -145,6 +145,7 @@ def test_create_and_delete_destination( "deleteme-destination-faker" + text_util.generate_random_suffix() ) destination_config = { + "destinationType": "duckdb", "destination_path": "temp_db", "motherduck_api_key": motherduck_api_key, } @@ -190,7 +191,7 @@ def test_create_and_delete_connection( name=new_source_name, api_root=airbyte_cloud_api_root, workspace_id=workspace_id, - config={"seed": None}, + config={"sourceType": "faker"}, client_id=airbyte_cloud_client_id, client_secret=airbyte_cloud_client_secret, bearer_token=None, @@ -204,6 +205,7 @@ def test_create_and_delete_connection( api_root=airbyte_cloud_api_root, workspace_id=workspace_id, config={ + "destinationType": "duckdb", "destination_path": "temp_db", "motherduck_api_key": motherduck_api_key, }, diff --git a/tests/unit_tests/test_cloud_api_util.py b/tests/unit_tests/test_cloud_api_util.py index db02ca2ae..8dbfaa0d4 100644 --- a/tests/unit_tests/test_cloud_api_util.py +++ b/tests/unit_tests/test_cloud_api_util.py @@ -3,6 +3,7 @@ from __future__ import annotations +import json from types import SimpleNamespace from unittest.mock import Mock from uuid import UUID @@ -766,6 +767,64 @@ def test_rename_workspace_forwards_request( assert captured[0]["request"].name == "Renamed workspace" +def test_public_api_request_sends_configuration_verbatim( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Connector configuration nulls are kept; top-level None fields are dropped.""" + captured_bodies: list[dict[str, object]] = [] + + def fake_request(**kwargs: object) -> SimpleNamespace: + captured_bodies.append(kwargs["json"]) + kind = "destination" if "destinations" in kwargs["url"] else "source" + response_body = { + f"{kind}Id": f"{kind}-1", + "name": "n", + f"{kind}Type": "faker", + "definitionId": "def-1", + "workspaceId": str(_id_for_index(0)), + "configuration": {}, + "createdAt": 1, + } + return SimpleNamespace( + status_code=200, + content=b"x", + json=lambda: response_body, + text=json.dumps(response_body), + ) + + monkeypatch.setattr(api_util.requests, "request", fake_request) + kwargs = { + "api_root": "https://api.airbyte.com/v1", + "client_id": None, + "client_secret": None, + "bearer_token": SecretString("token"), + } + + api_util.create_source( + name="n", + workspace_id=str(_id_for_index(0)), + config={"seed": None, "sourceType": "faker", "count": 5}, + **kwargs, + ) + api_util.patch_destination( + destination_id="destination-1", + config={"seed": None, "destinationType": "motherduck"}, + **kwargs, + ) + + assert captured_bodies[0]["configuration"] == { + "seed": None, + "sourceType": "faker", + "count": 5, + } + assert "definitionId" not in captured_bodies[0] + assert captured_bodies[1]["configuration"] == { + "seed": None, + "destinationType": "motherduck", + } + assert "name" not in captured_bodies[1] + + def test_patch_connection_normalizes_status_string( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/uv.lock b/uv.lock index 6918ec0a4..54180d528 100644 --- a/uv.lock +++ b/uv.lock @@ -188,7 +188,7 @@ dev = [ requires-dist = [ { name = "airbyte-cdk", specifier = ">=7.28.3,<8.0.0" }, { name = "airbyte-protocol-models-pdv2", specifier = ">=0.13.0,<1.0" }, - { name = "airbyte-server-models", git = "https://github.com/airbytehq/airbyte-ops-mcp.git?subdirectory=airbyte-server-models&rev=9a6e0ec03a96d3ac4d7bd83550ddad19da3fe96b" }, + { name = "airbyte-server-models", git = "https://github.com/airbytehq/airbyte-ops-mcp.git?subdirectory=airbyte-server-models&rev=b314c99aaa0ddc0e53ce79f94c1d82d26f94a5f0" }, { name = "cryptography", specifier = ">=44.0.0,!=45.0.0,!=45.0.1,<47.0.0" }, { name = "cyclopts", specifier = ">=4.0,<5.0" }, { name = "ddtrace", marker = "extra == 'datadog'", specifier = ">=4.15.2,<5.0" }, @@ -335,8 +335,8 @@ wheels = [ [[package]] name = "airbyte-server-models" -version = "0.0.0.post868.dev0+9a6e0ec" -source = { git = "https://github.com/airbytehq/airbyte-ops-mcp.git?subdirectory=airbyte-server-models&rev=9a6e0ec03a96d3ac4d7bd83550ddad19da3fe96b#9a6e0ec03a96d3ac4d7bd83550ddad19da3fe96b" } +version = "0.0.0.post872.dev0+b314c99" +source = { git = "https://github.com/airbytehq/airbyte-ops-mcp.git?subdirectory=airbyte-server-models&rev=b314c99aaa0ddc0e53ce79f94c1d82d26f94a5f0#b314c99aaa0ddc0e53ce79f94c1d82d26f94a5f0" } dependencies = [ { name = "pydantic", extra = ["email"] }, ]