diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 77a148b..6de6113 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -11,3 +11,19 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v3 - uses: pre-commit/action@v3.0.1 + + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v3 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Run tests + run: | + uv run pytest diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1be6a53 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f89ac11..c946ed2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ known_first_party = ["stac_auth_proxy"] profile = "black" [tool.ruff] -ignore = ["E501", "D203", "D212"] +ignore = ["E501", "D205", "D212"] select = ["D", "E", "F"] [build-system] @@ -39,4 +39,5 @@ requires = ["hatchling>=1.12.0"] [dependency-groups] dev = [ "pre-commit>=3.5.0", + "pytest>=8.3.3", ] diff --git a/src/stac_auth_proxy/__init__.py b/src/stac_auth_proxy/__init__.py index 3623015..35eaf7b 100644 --- a/src/stac_auth_proxy/__init__.py +++ b/src/stac_auth_proxy/__init__.py @@ -5,3 +5,8 @@ It includes FastAPI routes for handling authentication, authorization, and interaction with some internal STAC API. """ + +from .app import create_app +from .config import Settings + +__all__ = ["create_app", "Settings"] diff --git a/src/stac_auth_proxy/__main__.py b/src/stac_auth_proxy/__main__.py index db92b7d..42d97b5 100644 --- a/src/stac_auth_proxy/__main__.py +++ b/src/stac_auth_proxy/__main__.py @@ -1,4 +1,4 @@ -"""Main module for the STAC Auth Proxy.""" +"""Entry point for running the module without customized code.""" import uvicorn from uvicorn.config import LOGGING_CONFIG diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..475ca63 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,56 @@ +"""Pytest fixtures.""" + +import threading + +import pytest +import uvicorn +from fastapi import FastAPI + + +@pytest.fixture(scope="session") +def source_api(): + """Create upstream API for testing purposes.""" + app = FastAPI(docs_url="/api.html", openapi_url="/api") + + for path, methods in { + "/": ["GET"], + "/conformance": ["GET"], + "/queryables": ["GET"], + "/search": ["GET", "POST"], + "/collections": ["GET", "POST"], + "/collections/{collection_id}": ["GET", "PUT", "DELETE"], + "/collections/{collection_id}/items": ["GET", "POST"], + "/collections/{collection_id}/items/{item_id}": [ + "GET", + "PUT", + "DELETE", + ], + "/collections/{collection_id}/bulk_items": ["POST"], + }.items(): + for method in methods: + # NOTE: declare routes per method separately to avoid warning of "Duplicate Operation ID ... for function " + app.add_api_route( + path, + lambda: {"id": f"Response from {method}@{path}"}, + methods=[method], + ) + + return app + + +@pytest.fixture(scope="session") +def source_api_server(source_api): + """Run the source API in a background thread.""" + host, port = "127.0.0.1", 8000 + server = uvicorn.Server( + uvicorn.Config( + source_api, + host=host, + port=port, + ) + ) + thread = threading.Thread(target=server.run) + thread.start() + yield f"http://{host}:{port}" + server.should_exit = True + thread.join() diff --git a/tests/test_defaults.py b/tests/test_defaults.py new file mode 100644 index 0000000..24a06ac --- /dev/null +++ b/tests/test_defaults.py @@ -0,0 +1,105 @@ +"""Basic test cases for the proxy app.""" + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from stac_auth_proxy import Settings, create_app + + +@pytest.fixture(scope="module") +def test_app(source_api_server: str) -> FastAPI: + """Fixture for the proxy app, pointing to the source API.""" + return create_app( + Settings.model_validate( + { + "upstream_url": source_api_server, + "oidc_discovery_url": "https://samples.auth0.com/.well-known/openid-configuration", + "default_public": False, + }, + ) + ) + + +@pytest.mark.parametrize( + "path,method,expected_status", + [ + ("/", "GET", 200), + ("/conformance", "GET", 200), + ("/queryables", "GET", 200), + ("/search", "GET", 200), + ("/search", "POST", 200), + ("/collections", "GET", 200), + ("/collections", "POST", 403), + ("/collections/example-collection", "GET", 200), + ("/collections/example-collection", "PUT", 403), + ("/collections/example-collection", "DELETE", 403), + ("/collections/example-collection/items", "GET", 200), + ("/collections/example-collection/items", "POST", 403), + ("/collections/example-collection/items/example-item", "GET", 200), + ("/collections/example-collection/items/example-item", "PUT", 403), + ("/collections/example-collection/items/example-item", "DELETE", 403), + ("/collections/example-collection/bulk_items", "POST", 403), + ("/api.html", "GET", 200), + ("/api", "GET", 200), + ], +) +def test_default_public_true(source_api_server, path, method, expected_status): + """ + When default_public=true and private_endpoints aren't set, all endpoints should be + public except for transaction endpoints. + """ + test_app = create_app( + Settings.model_validate( + { + "upstream_url": source_api_server, + "oidc_discovery_url": "https://samples.auth0.com/.well-known/openid-configuration", + "default_public": True, + }, + ) + ) + client = TestClient(test_app) + response = client.request(method=method, url=path) + assert response.status_code == expected_status + + +@pytest.mark.parametrize( + "path,method,expected_status", + [ + ("/", "GET", 403), + ("/conformance", "GET", 403), + ("/queryables", "GET", 403), + ("/search", "GET", 403), + ("/search", "POST", 403), + ("/collections", "GET", 403), + ("/collections", "POST", 403), + ("/collections/example-collection", "GET", 403), + ("/collections/example-collection", "PUT", 403), + ("/collections/example-collection", "DELETE", 403), + ("/collections/example-collection/items", "GET", 403), + ("/collections/example-collection/items", "POST", 403), + ("/collections/example-collection/items/example-item", "GET", 403), + ("/collections/example-collection/items/example-item", "PUT", 403), + ("/collections/example-collection/items/example-item", "DELETE", 403), + ("/collections/example-collection/bulk_items", "POST", 403), + ("/api.html", "GET", 200), + ("/api", "GET", 200), + ], +) +def test_default_public_false(source_api_server, path, method, expected_status): + """ + When default_public=false and private_endpoints aren't set, all endpoints should be + public except for transaction endpoints. + """ + test_app = create_app( + Settings.model_validate( + { + "upstream_url": source_api_server, + "oidc_discovery_url": "https://samples.auth0.com/.well-known/openid-configuration", + "default_public": False, + }, + ) + ) + client = TestClient(test_app) + response = client.request(method=method, url=path) + assert response.status_code == expected_status diff --git a/tests/test_openapi.py b/tests/test_openapi.py new file mode 100644 index 0000000..63a1a95 --- /dev/null +++ b/tests/test_openapi.py @@ -0,0 +1,44 @@ +"""Tests for OpenAPI spec handling.""" + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from stac_auth_proxy import Settings, create_app + + +def test_no_edit_openapi_spec(source_api_server): + """When no OpenAPI spec endpoint is set, the proxied OpenAPI spec is unaltered.""" + app = create_app( + Settings( + upstream_url=source_api_server, + oidc_discovery_url="https://samples.auth0.com/.well-known/openid-configuration", + openapi_spec_endpoint=None, + ) + ) + client = TestClient(app) + response = client.get("/api") + assert response.status_code == 200 + openapi = response.json() + assert "info" in openapi + assert "openapi" in openapi + assert "paths" in openapi + assert "oidcAuth" not in openapi.get("components", {}).get("securitySchemes", {}) + + +def test_oidc_in_openapi_spec(source_api: FastAPI, source_api_server: str): + """When OpenAPI spec endpoint is set, the proxied OpenAPI spec is augmented with oidc details.""" + app = create_app( + Settings( + upstream_url=source_api_server, + oidc_discovery_url="https://samples.auth0.com/.well-known/openid-configuration", + openapi_spec_endpoint=source_api.openapi_url, + ) + ) + client = TestClient(app) + response = client.get(source_api.openapi_url) + assert response.status_code == 200 + openapi = response.json() + assert "info" in openapi + assert "openapi" in openapi + assert "paths" in openapi + assert "oidcAuth" in openapi.get("components", {}).get("securitySchemes", {}) diff --git a/uv.lock b/uv.lock index 09fc13a..b49bf07 100644 --- a/uv.lock +++ b/uv.lock @@ -393,6 +393,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -402,6 +411,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + [[package]] name = "platformdirs" version = "4.3.6" @@ -411,6 +429,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, ] +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + [[package]] name = "pre-commit" version = "3.5.0" @@ -573,6 +600,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, ] +[[package]] +name = "pytest" +version = "8.3.3" +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 = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -667,6 +711,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "pre-commit" }, + { name = "pytest" }, ] [package.metadata] @@ -680,7 +725,10 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "pre-commit", specifier = ">=3.5.0" }] +dev = [ + { name = "pre-commit", specifier = ">=3.5.0" }, + { name = "pytest", specifier = ">=8.3.3" }, +] [[package]] name = "starlette" @@ -695,6 +743,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, ] +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + [[package]] name = "typing-extensions" version = "4.12.2"