From b83885a26e1709e91dd4c958e17a9c41b1069156 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 11 Dec 2024 23:08:08 -0800 Subject: [PATCH 01/29] Rm CEL --- README.md | 1 - src/stac_auth_proxy/app.py | 4 -- src/stac_auth_proxy/config.py | 2 - src/stac_auth_proxy/guards/__init__.py | 5 -- src/stac_auth_proxy/guards/cel.py | 45 --------------- tests/test_guards_cel.py | 76 -------------------------- 6 files changed, 133 deletions(-) delete mode 100644 src/stac_auth_proxy/guards/__init__.py delete mode 100644 src/stac_auth_proxy/guards/cel.py delete mode 100644 tests/test_guards_cel.py diff --git a/README.md b/README.md index f071904..f49dcc9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ STAC Auth Proxy is a proxy API that mediates between the client and and some int - 🔐 Selectively apply OIDC auth to some or all endpoints & methods - 📖 Augments [OpenAPI](https://swagger.io/specification/) with auth information, keeping auto-generated docs (e.g. [Swagger UI](https://swagger.io/tools/swagger-ui/)) accurate -- 💂‍♀️ Custom policies enforce complex access controls, defined with [Common Expression Language (CEL)](https://cel.dev/) ## Installation diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 9f3f83f..83776da 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -31,10 +31,6 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: openid_configuration_url=str(settings.oidc_discovery_url) ).valid_token_dependency - if settings.guard: - logger.info("Wrapping auth scheme") - auth_scheme = settings.guard(auth_scheme) - if settings.debug: app.add_api_route( "/_debug", diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index cb96c5d..4e61cf8 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -49,5 +49,3 @@ class Settings(BaseSettings): openapi_spec_endpoint: Optional[str] = None model_config = SettingsConfigDict(env_prefix="STAC_AUTH_PROXY_") - - guard: Optional[ClassInput] = None diff --git a/src/stac_auth_proxy/guards/__init__.py b/src/stac_auth_proxy/guards/__init__.py deleted file mode 100644 index 4dda044..0000000 --- a/src/stac_auth_proxy/guards/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Module to define the access policy guards for the application.""" - -from .cel import cel - -__all__ = ["cel"] diff --git a/src/stac_auth_proxy/guards/cel.py b/src/stac_auth_proxy/guards/cel.py deleted file mode 100644 index 9b957e2..0000000 --- a/src/stac_auth_proxy/guards/cel.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Guard using CEL (Common Expression Language, https://cel.dev).""" - -from typing import Any, Callable - -import celpy -from fastapi import HTTPException, Request, Security - -from ..utils import extract_variables - - -def cel(expression: str, token_dependency: Callable[..., Any]): - """Cel check factory.""" - env = celpy.Environment() - ast = env.compile(expression) - program = env.program(ast) - - async def check( - request: Request, - auth_token=Security(token_dependency), - ): - request_data = { - "path": request.url.path, - "method": request.method, - "query_params": dict(request.query_params), - "path_params": extract_variables(request.url.path), - "headers": dict(request.headers), - "body": ( - await request.json() - if request.headers.get("content-type") == "application/json" - else (await request.body()).decode() - ), - } - - result = program.evaluate( - celpy.json_to_cel( - { - "req": request_data, - "token": auth_token, - } - ) - ) - if not result: - raise HTTPException(status_code=403, detail="Forbidden (failed CEL check)") - - return check diff --git a/tests/test_guards_cel.py b/tests/test_guards_cel.py deleted file mode 100644 index 84e705f..0000000 --- a/tests/test_guards_cel.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Tests for CEL guard.""" - -import pytest -from fastapi.testclient import TestClient -from utils import AppFactory - -app_factory = AppFactory( - oidc_discovery_url="https://example-stac-api.com/.well-known/openid-configuration", - default_public=False, -) - - -@pytest.mark.parametrize( - "endpoint, expected_status_code", - [ - ("/", 403), - ("/?foo=xyz", 403), - ("/?bar=foo", 403), - ("/?foo=bar", 200), - ("/?foo=xyz&foo=bar", 200), # Only the last value is checked - ("/?foo=bar&foo=xyz", 403), # Only the last value is checked - ], -) -def test_guard_query_params( - source_api_server, - token_builder, - endpoint, - expected_status_code, -): - """Test guard with query parameters.""" - app = app_factory( - upstream_url=source_api_server, - guard={ - "cls": "stac_auth_proxy.guards.cel", - "args": ('has(req.query_params.foo) && req.query_params.foo == "bar"',), - }, - ) - client = TestClient(app, headers={"Authorization": f"Bearer {token_builder({})}"}) - response = client.get(endpoint) - assert response.status_code == expected_status_code - - -@pytest.mark.parametrize( - "token_payload, expected_status_code", - [ - ({"foo": "bar"}, 403), - ({"collections": []}, 403), - ({"collections": ["foo", "bar"]}, 403), - ({"collections": ["xyz"]}, 200), - ({"collections": ["foo", "xyz"]}, 200), - ], -) -def test_guard_auth_token( - source_api_server, - token_builder, - token_payload, - expected_status_code, -): - """Test guard with auth token.""" - app = app_factory( - upstream_url=source_api_server, - guard={ - "cls": "stac_auth_proxy.guards.cel", - "args": ( - """ - has(req.path_params.collection_id) && has(token.collections) && - req.path_params.collection_id in (token.collections) - """, - ), - }, - ) - client = TestClient( - app, headers={"Authorization": f"Bearer {token_builder(token_payload)}"} - ) - response = client.get("/collections/xyz") - assert response.status_code == expected_status_code From fe48623ea980b5d50ca8e441939eeadfc7d7c271 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 11 Dec 2024 23:08:37 -0800 Subject: [PATCH 02/29] Bring eoapi-auth-utils into this lib, customize to permit optional auth --- src/stac_auth_proxy/app.py | 5 +- src/stac_auth_proxy/auth.py | 117 ++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 src/stac_auth_proxy/auth.py diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 83776da..faaa7c7 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -8,9 +8,9 @@ import logging from typing import Optional -from eoapi.auth_utils import OpenIdConnectAuth from fastapi import Depends, FastAPI +from .auth import OpenIdConnectAuth from .config import Settings from .handlers import OpenApiSpecHandler, ReverseProxyHandler from .middleware import AddProcessTimeHeaderMiddleware @@ -40,7 +40,8 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: proxy_handler = ReverseProxyHandler(upstream=str(settings.upstream_url)) openapi_handler = OpenApiSpecHandler( - proxy=proxy_handler, oidc_config_url=str(settings.oidc_discovery_url) + proxy=proxy_handler, + oidc_config_url=str(settings.oidc_discovery_url), ) # Endpoints that are explicitely marked private diff --git a/src/stac_auth_proxy/auth.py b/src/stac_auth_proxy/auth.py new file mode 100644 index 0000000..d46d9d2 --- /dev/null +++ b/src/stac_auth_proxy/auth.py @@ -0,0 +1,117 @@ +import json +import logging +import urllib.request +from dataclasses import dataclass, field +from typing import Annotated, Any, Callable, Optional, Sequence + +import jwt +from fastapi import HTTPException, Security, security, status +from fastapi.security.base import SecurityBase +from starlette.exceptions import HTTPException +from starlette.status import HTTP_403_FORBIDDEN +from pydantic import AnyHttpUrl + + +logger = logging.getLogger(__name__) + + +@dataclass +class OpenIdConnectAuth: + openid_configuration_url: AnyHttpUrl + openid_configuration_internal_url: Optional[AnyHttpUrl] = None + allowed_jwt_audiences: Optional[Sequence[str]] = None + + # Generated attributes + auth_scheme: SecurityBase = field(init=False) + jwks_client: jwt.PyJWKClient = field(init=False) + valid_token_dependency: Callable[..., Any] = field(init=False) + + def __post_init__(self): + logger.debug("Requesting OIDC config") + with urllib.request.urlopen( + str(self.openid_configuration_internal_url or self.openid_configuration_url) + ) as response: + if response.status != 200: + logger.error( + "Received a non-200 response when fetching OIDC config: %s", + response.text, + ) + raise OidcFetchError( + f"Request for OIDC config failed with status {response.status}" + ) + oidc_config = json.load(response) + self.jwks_client = jwt.PyJWKClient(oidc_config["jwks_uri"]) + + self.valid_token_dependency.__annotations__["auth_header"] = ( + security.OpenIdConnect( + openIdConnectUrl=str(self.openid_configuration_url), auto_error=True + ) + ) + + def user_or_none(self, auth_header: Annotated[str, Security(auth_scheme)]): + """Return the validated user if authenticated, else None.""" + return self.valid_token_dependency( + auth_header, security.SecurityScopes([]), auto_error=False + ) + + def valid_token_dependency( + self, + auth_header: Annotated[str, Security(auth_scheme)], + required_scopes: security.SecurityScopes, + auto_error: bool = True, + ): + """Dependency to validate an OIDC token.""" + if not auth_header: + if auto_error: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" + ) + return None + + # Extract token from header + token_parts = auth_header.split(" ") + if len(token_parts) != 2 or token_parts[0].lower() != "bearer": + logger.error(f"Invalid token: {auth_header}") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + [_, token] = token_parts + + # Parse & validate token + try: + key = self.jwks_client.get_signing_key_from_jwt(token).key + payload = jwt.decode( + token, + key, + algorithms=["RS256"], + # NOTE: Audience validation MUST match audience claim if set in token (https://pyjwt.readthedocs.io/en/stable/changelog.html?highlight=audience#id40) + audience=self.allowed_jwt_audiences, + ) + except (jwt.exceptions.InvalidTokenError, jwt.exceptions.DecodeError) as e: + logger.exception(f"InvalidTokenError: {e=}") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) from e + + # Validate scopes (if required) + for scope in required_scopes.scopes: + if scope not in payload["scope"]: + if auto_error: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={ + "WWW-Authenticate": f'Bearer scope="{required_scopes.scope_str}"' + }, + ) + return None + + return payload + + +class OidcFetchError(Exception): + pass From ebe494ba600aabd4d0f2c781342231f68885b505 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 11 Dec 2024 23:09:41 -0800 Subject: [PATCH 03/29] Add start to filters --- src/stac_auth_proxy/config.py | 6 ++++ src/stac_auth_proxy/filters/__init__.py | 3 ++ src/stac_auth_proxy/filters/template.py | 44 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 src/stac_auth_proxy/filters/__init__.py create mode 100644 src/stac_auth_proxy/filters/template.py diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index 4e61cf8..8ea16f6 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -48,4 +48,10 @@ class Settings(BaseSettings): public_endpoints: EndpointMethods = {"/api.html": ["GET"], "/api": ["GET"]} openapi_spec_endpoint: Optional[str] = None + collections_filter: Optional[ClassInput] = { + "cls": "stac_auth_proxy.filters.Template", + "args": ["""A_CONTAINEDBY(id, ( '{{ token.collections | join("', '") }}' ))"""], + } + items_filter: Optional[ClassInput] = None + model_config = SettingsConfigDict(env_prefix="STAC_AUTH_PROXY_") diff --git a/src/stac_auth_proxy/filters/__init__.py b/src/stac_auth_proxy/filters/__init__.py new file mode 100644 index 0000000..35f216f --- /dev/null +++ b/src/stac_auth_proxy/filters/__init__.py @@ -0,0 +1,3 @@ +from .template import Template + +__all__ = ["Template"] diff --git a/src/stac_auth_proxy/filters/template.py b/src/stac_auth_proxy/filters/template.py new file mode 100644 index 0000000..ffaea42 --- /dev/null +++ b/src/stac_auth_proxy/filters/template.py @@ -0,0 +1,44 @@ +from typing import Any, Callable + +from cql2 import Expr +from jinja2 import Environment, BaseLoader +from fastapi import Request, Security + +from ..utils import extract_variables + +from dataclasses import dataclass, field + + +@dataclass +class Template: + template_str: str + token_dependency: Callable[..., Any] + + # Generated attributes + env: Environment = field(init=False) + + def __post_init__(self): + self.env = Environment(loader=BaseLoader).from_string(self.template_str) + self.render.__annotations__["auth_token"] = Security(self.token_dependency) + + async def cql2(self, request: Request, auth_token=Security(...)) -> Expr: + # TODO: How to handle the case where auth_token is null? + context = { + "req": { + "path": request.url.path, + "method": request.method, + "query_params": dict(request.query_params), + "path_params": extract_variables(request.url.path), + "headers": dict(request.headers), + "body": ( + await request.json() + if request.headers.get("content-type") == "application/json" + else (await request.body()).decode() + ), + }, + "token": auth_token, + } + cql2_str = self.env.render(**context) + cql2_expr = Expr(cql2_str) + cql2_expr.validate() + return cql2_expr From fa70d684d6db8edc85f92c4223b7b49ae108561c Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 11 Dec 2024 23:09:50 -0800 Subject: [PATCH 04/29] Update requirements --- pyproject.toml | 7 +- uv.lock | 450 +++++++++++++++++++------------------------------ 2 files changed, 182 insertions(+), 275 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 46f8d55..5de22e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,11 +8,12 @@ classifiers = [ dependencies = [ "authlib>=1.3.2", "brotli>=1.1.0", - "cel-python>=0.1.5", - "eoapi-auth-utils>=0.4.0", + "cql2>=0.3.2", "fastapi>=0.115.5", "httpx>=0.28.0", + "jinja2>=3.1.4", "pydantic-settings>=2.6.1", + "pyjwt>=2.10.1", "uvicorn>=0.32.1", ] description = "STAC authentication proxy with FastAPI" @@ -20,7 +21,7 @@ keywords = ["STAC", "FastAPI", "Authentication", "Proxy"] license = {file = "LICENSE"} name = "stac-auth-proxy" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" version = "0.1.0" [tool.coverage.run] diff --git a/uv.lock b/uv.lock index 8da1db6..17f076e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,13 +1,10 @@ version = 1 -requires-python = ">=3.8" +requires-python = ">=3.9" [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.9'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, @@ -40,18 +37,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/4c/9aa0416a403d5cc80292cb030bcd2c918cce2755e314d8c1aa18656e1e12/Authlib-1.3.2-py2.py3-none-any.whl", hash = "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc", size = 225111 }, ] -[[package]] -name = "babel" -version = "2.16.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytz", marker = "python_full_version < '3.9'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, -] - [[package]] name = "brotli" version = "1.1.0" @@ -120,22 +105,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206 }, { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804 }, { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517 }, - { url = "https://files.pythonhosted.org/packages/34/1b/16114a20c0a43c20331f03431178ed8b12280b12c531a14186da0bc5b276/Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", size = 873053 }, - { url = "https://files.pythonhosted.org/packages/36/49/2afe4aa5a23a13dad4c7160ae574668eec58b3c80b56b74a826cebff7ab8/Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", size = 446211 }, - { url = "https://files.pythonhosted.org/packages/10/9d/6463edb80a9e0a944f70ed0c4d41330178526626d7824f729e81f78a3f24/Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", size = 2904604 }, - { url = "https://files.pythonhosted.org/packages/a4/bd/cfaac88c14f97d9e1f2e51a304c3573858548bb923d011b19f76b295f81c/Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", size = 2941707 }, - { url = "https://files.pythonhosted.org/packages/60/3f/2618fa887d7af6828246822f10d9927244dab22db7a96ec56041a2fd1fbd/Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", size = 2672420 }, - { url = "https://files.pythonhosted.org/packages/e7/41/1c6d15c8d5b55db2c3c249c64c352c8a1bc97f5e5c55183f5930866fc012/Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", size = 2757410 }, - { url = "https://files.pythonhosted.org/packages/6c/5b/ca72fd8aa1278dfbb12eb320b6e409aefabcd767b85d607c9d54c9dadd1a/Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", size = 2911143 }, - { url = "https://files.pythonhosted.org/packages/b1/53/110657f4017d34a2e9a96d9630a388ad7e56092023f1d46d11648c6c0bce/Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", size = 2809968 }, - { url = "https://files.pythonhosted.org/packages/3f/2a/fbc95429b45e4aa4a3a3a815e4af11772bfd8ef94e883dcff9ceaf556662/Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088", size = 2935402 }, - { url = "https://files.pythonhosted.org/packages/4e/52/02acd2992e5a2c10adf65fa920fad0c29e11e110f95eeb11bcb20342ecd2/Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", size = 2931208 }, - { url = "https://files.pythonhosted.org/packages/6b/35/5d258d1aeb407e1fc6fcbbff463af9c64d1ecc17042625f703a1e9d22ec5/Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7", size = 2933171 }, - { url = "https://files.pythonhosted.org/packages/cc/58/b25ca26492da9880e517753967685903c6002ddc2aade93d6e56df817b30/Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5", size = 2845347 }, - { url = "https://files.pythonhosted.org/packages/12/cf/91b84beaa051c9376a22cc38122dc6fbb63abcebd5a4b8503e9c388de7b1/Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943", size = 3031668 }, - { url = "https://files.pythonhosted.org/packages/38/05/04a57ba75aed972be0c6ad5f2f5ea34c83f5fecf57787cc6e54aac21a323/Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a", size = 2926949 }, - { url = "https://files.pythonhosted.org/packages/c9/2f/fbe6938f33d2cd9b7d7fb591991eb3fb57ffa40416bb873bbbacab60a381/Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", size = 333179 }, - { url = "https://files.pythonhosted.org/packages/39/a5/9322c8436072e77b8646f6bde5e19ee66f62acf7aa01337ded10777077fa/Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", size = 357254 }, { url = "https://files.pythonhosted.org/packages/1b/aa/aa6e0c9848ee4375514af0b27abf470904992939b7363ae78fc8aca8a9a8/Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", size = 873048 }, { url = "https://files.pythonhosted.org/packages/ae/32/38bba1a8bef9ecb1cda08439fd28d7e9c51aff13b4783a4f1610da90b6c2/Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", size = 446207 }, { url = "https://files.pythonhosted.org/packages/3c/6a/14cc20ddc53efc274601c8195791a27cfb7acc5e5134e0f8c493a8b8821a/Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", size = 2903803 }, @@ -154,24 +123,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/b3/f7b3af539f74b82e1c64d28685a5200c631cc14ae751d37d6ed819655627/Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", size = 357258 }, ] -[[package]] -name = "cel-python" -version = "0.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "babel" }, - { name = "jmespath" }, - { name = "lark-parser" }, - { name = "python-dateutil" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/9e/c3af4a83cbe8108cff92b0588b7ac53efc41fcaf1ffd09d07dc68c9b5d27/cel-python-0.1.5.tar.gz", hash = "sha256:d3911bb046bc3ed12792bd88ab453f72d98c66923b72a2fa016bcdffd96e2f98", size = 81518 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/6b/e12b1d5593cbf49d52028bd5ace95ec3e4197365b19ea4ed198c7c06f495/cel_python-0.1.5-py3-none-any.whl", hash = "sha256:ac81fab8ba08b633700a45d84905be2863529c6a32935c9da7ef53fc06844f1a", size = 87124 }, -] - [[package]] name = "certifi" version = "2024.8.30" @@ -236,14 +187,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, - { url = "https://files.pythonhosted.org/packages/48/08/15bf6b43ae9bd06f6b00ad8a91f5a8fe1069d4c9fab550a866755402724e/cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", size = 182457 }, - { url = "https://files.pythonhosted.org/packages/c2/5b/f1523dd545f92f7df468e5f653ffa4df30ac222f3c884e51e139878f1cb5/cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", size = 425932 }, - { url = "https://files.pythonhosted.org/packages/53/93/7e547ab4105969cc8c93b38a667b82a835dd2cc78f3a7dad6130cfd41e1d/cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", size = 448585 }, - { url = "https://files.pythonhosted.org/packages/56/c4/a308f2c332006206bb511de219efeff090e9d63529ba0a77aae72e82248b/cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", size = 456268 }, - { url = "https://files.pythonhosted.org/packages/ca/5b/b63681518265f2f4060d2b60755c1c77ec89e5e045fc3773b72735ddaad5/cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", size = 436592 }, - { url = "https://files.pythonhosted.org/packages/bb/19/b51af9f4a4faa4a8ac5a0e5d5c2522dcd9703d07fac69da34a36c4d960d3/cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", size = 446512 }, - { url = "https://files.pythonhosted.org/packages/e2/63/2bed8323890cb613bbecda807688a31ed11a7fe7afe31f8faaae0206a9a3/cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", size = 171576 }, - { url = "https://files.pythonhosted.org/packages/2f/70/80c33b044ebc79527447fd4fbc5455d514c3bb840dede4455de97da39b4d/cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", size = 181229 }, { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, @@ -267,105 +210,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, ] -[[package]] -name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, - { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, - { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, - { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, - { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, - { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, - { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, - { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, - { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, - { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, - { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, - { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, - { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, - { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, - { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, - { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, - { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, - { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, - { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, - { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, - { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, - { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, - { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, - { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, - { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, - { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, - { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, - { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, - { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, - { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, - { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, - { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, - { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, - { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, - { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, - { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, - { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, - { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, - { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, - { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, - { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, - { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, - { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, - { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, - { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, - { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, - { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, - { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, - { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, - { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, - { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, - { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, - { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, - { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, - { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, - { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, - { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, - { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, - { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, - { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, - { url = "https://files.pythonhosted.org/packages/86/f4/ccab93e631e7293cca82f9f7ba39783c967f823a0000df2d8dd743cad74f/charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", size = 193961 }, - { url = "https://files.pythonhosted.org/packages/94/d4/2b21cb277bac9605026d2d91a4a8872bc82199ed11072d035dc674c27223/charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", size = 124507 }, - { url = "https://files.pythonhosted.org/packages/9a/e0/a7c1fcdff20d9c667342e0391cfeb33ab01468d7d276b2c7914b371667cc/charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", size = 119298 }, - { url = "https://files.pythonhosted.org/packages/70/de/1538bb2f84ac9940f7fa39945a5dd1d22b295a89c98240b262fc4b9fcfe0/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", size = 139328 }, - { url = "https://files.pythonhosted.org/packages/e9/ca/288bb1a6bc2b74fb3990bdc515012b47c4bc5925c8304fc915d03f94b027/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", size = 149368 }, - { url = "https://files.pythonhosted.org/packages/aa/75/58374fdaaf8406f373e508dab3486a31091f760f99f832d3951ee93313e8/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", size = 141944 }, - { url = "https://files.pythonhosted.org/packages/32/c8/0bc558f7260db6ffca991ed7166494a7da4fda5983ee0b0bfc8ed2ac6ff9/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", size = 143326 }, - { url = "https://files.pythonhosted.org/packages/0e/dd/7f6fec09a1686446cee713f38cf7d5e0669e0bcc8288c8e2924e998cf87d/charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", size = 146171 }, - { url = "https://files.pythonhosted.org/packages/4c/a8/440f1926d6d8740c34d3ca388fbd718191ec97d3d457a0677eb3aa718fce/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", size = 139711 }, - { url = "https://files.pythonhosted.org/packages/e9/7f/4b71e350a3377ddd70b980bea1e2cc0983faf45ba43032b24b2578c14314/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", size = 148348 }, - { url = "https://files.pythonhosted.org/packages/1e/70/17b1b9202531a33ed7ef41885f0d2575ae42a1e330c67fddda5d99ad1208/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", size = 151290 }, - { url = "https://files.pythonhosted.org/packages/44/30/574b5b5933d77ecb015550aafe1c7d14a8cd41e7e6c4dcea5ae9e8d496c3/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", size = 149114 }, - { url = "https://files.pythonhosted.org/packages/0b/11/ca7786f7e13708687443082af20d8341c02e01024275a28bc75032c5ce5d/charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", size = 143856 }, - { url = "https://files.pythonhosted.org/packages/f9/c2/1727c1438256c71ed32753b23ec2e6fe7b6dff66a598f6566cfe8139305e/charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", size = 94333 }, - { url = "https://files.pythonhosted.org/packages/09/c8/0e17270496a05839f8b500c1166e3261d1226e39b698a735805ec206967b/charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", size = 101454 }, - { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, - { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, - { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, - { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, - { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, - { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, - { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, - { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, - { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, - { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, - { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, - { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, - { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, - { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, - { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, -] - [[package]] name = "click" version = "8.1.7" @@ -443,16 +287,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, - { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674 }, - { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101 }, - { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554 }, - { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440 }, - { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889 }, - { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142 }, - { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805 }, - { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655 }, - { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296 }, - { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137 }, { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 }, { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 }, { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 }, @@ -471,6 +305,104 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cql2" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/8e/c72163ba6cca587550b98c3ce697a3061ac78c5a73516aa10444055d63ed/cql2-0.3.2.tar.gz", hash = "sha256:1ea771cb72847d32b7e5fe502fc3e42dd992eb7f178ccb50c91207d7937a918c", size = 124394 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/7d/c391fd429e857887bbb4fa8080465b1fd873d30aa78fe482868c6918c206/cql2-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fc41c4af5421a7e6f6f0dfd4af702e7e7074ceb7e96653ea1394578f0cd43fb", size = 2135591 }, + { url = "https://files.pythonhosted.org/packages/38/b3/3dde673066cbd134af30b6d75976ba6d5efc09bf4380607df2c7955f9209/cql2-0.3.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b53a6e6d32697b62f867f2e34975668c2039252fc4c5b5e9e538ca5535a8ea", size = 2084471 }, + { url = "https://files.pythonhosted.org/packages/f1/03/60adcdab78ac24e80cceeee953c03927c683ec3387d14781481c5125eef3/cql2-0.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fdaf3ba0a9516e7bd9e4e1f060a6fb417d2813e1872095339a2058af0264e4e", size = 2303166 }, + { url = "https://files.pythonhosted.org/packages/06/fe/a2bd2ea7ad5866ff7ce8e74f70d9d958a1ca9148acb367bb8f6234ca8cef/cql2-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23b5f857042276862bc7a00dc4e9acd07bf2341a61d5f0c1496375e15453f748", size = 2319045 }, + { url = "https://files.pythonhosted.org/packages/c6/ab/5a61d6b5de191daeb4e711f8ca053776b8dc30be599df6bd9ce4b8312688/cql2-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb8480a7b2456fb05f5a64087c3a4ec76a6902807e88600e398da11615437ee4", size = 2428807 }, + { url = "https://files.pythonhosted.org/packages/58/96/da1830b8ffca626bc1a29ce05a32bd9eaa0948048e009f641a38db5568e6/cql2-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:454bd22539ff74639917ca7d808786a93498032018876796e8348c8f1af2e35d", size = 2235250 }, + { url = "https://files.pythonhosted.org/packages/5c/66/a20a8b32054c5ff05589a0041c3c8e80035f21717892527435cb8168227f/cql2-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d14dc8949f0876b9b98011bcbcfecca14441a2bfa16c47c61c82051370dc5dec", size = 2310443 }, + { url = "https://files.pythonhosted.org/packages/c6/2b/aa2a499c9c506437a4960cf108694d39eadbc21f4a95c523f8306cb9f364/cql2-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:263c59f9246b7b6e312765bc5b9bf3f4edc6105c829ba299856896a343fc00b5", size = 2346771 }, + { url = "https://files.pythonhosted.org/packages/05/f2/f0cb0423383f4547be63111c21c44a65bfde14bf036f32ef46d9cdef9d0c/cql2-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:761d0bfc7f57ed8a19a81fc3a76f942f8919643e4fc5af92288b85ad7464cdbd", size = 2372189 }, + { url = "https://files.pythonhosted.org/packages/f7/52/da959efa5c3490d11e6bc948eb621bc97210648dc669b762187c077b13d6/cql2-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:041e75d902c25ae64dc02b33a94281f103b716147dd7acd0c3372d8dc7800203", size = 2403571 }, + { url = "https://files.pythonhosted.org/packages/df/fd/daa674c3df2ee9c46706f6adbab6d17fbb9a29f929f530de937689fd496e/cql2-0.3.2-cp310-cp310-win32.whl", hash = "sha256:127f4bf4aee3dadf725db8e1737c6434ab06cbdc8cc0caec1dc1cc223458c345", size = 1642521 }, + { url = "https://files.pythonhosted.org/packages/0c/89/0539b5f643dcd452dec034966cd5c48455d9374371347304bff819dffac2/cql2-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:232e9408f65bae6f0c03e6e9e1df6d322e6ab64e0df3acda49891ab296e8f46d", size = 1781391 }, + { url = "https://files.pythonhosted.org/packages/44/d4/667f850a911b8244d2f2dfbc47288eaf06cb776b72b178b440255d89620b/cql2-0.3.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:62bb9070e0eab2ced35687be50522182b651be1ea0c87ea4d4b256bc5848b918", size = 2031021 }, + { url = "https://files.pythonhosted.org/packages/c0/01/5f953a7ded0d272fab288cb9498f8acf42a60d6e9bc8cb002b231de621a4/cql2-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0b3ca6c7c7a4c6eec270a7909636efcf78006bd5d7cf9f3dfa1985c8ecbbce9c", size = 1936400 }, + { url = "https://files.pythonhosted.org/packages/61/00/c57156af197918259ce5aac0bebea1648b3a9f285266076649f329b07364/cql2-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd5d2a973fe33dea675f320de010bd063ba1c26d28675e4cdbbefaff51235ed", size = 2133274 }, + { url = "https://files.pythonhosted.org/packages/60/ea/db588c9f5b9e1f395543bf6c5ab2574bc353bf6c913a13b48b221b9db5e8/cql2-0.3.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f374c9417d70962512674e32ff280d04246b24cbf43b05774b1bb68481e712d", size = 2084941 }, + { url = "https://files.pythonhosted.org/packages/00/4e/6d7675bbb4096ab335839814a38b41990ebdcfc00725f0fbac09a7f2320d/cql2-0.3.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ac0fe125e49cc4c220baed6ef057079436113dbeabc5bf01f17ceaea0e19975", size = 2302051 }, + { url = "https://files.pythonhosted.org/packages/af/af/f8f2735aef7598c6f20592ca6835e42c50e16e2bece96caaf2937321fb3b/cql2-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88c8ee0a9a924b13108008a91aa2721a89e68e741fbaaf83ddfa7d3b931d98f8", size = 2319509 }, + { url = "https://files.pythonhosted.org/packages/4c/24/b5ce0853e2dcecb6bf3a6c53749d3d6804a243a0f42e8ec000725f7d02fe/cql2-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6a7a5213fd3f4ae6fd707654027eaf3d9d13c12f873ec88d13cbf510b1cdc4", size = 2427855 }, + { url = "https://files.pythonhosted.org/packages/1f/17/efa0f0c15882218160f637180ba79ea0b13c26e2a32c60eeb005fcc9c471/cql2-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ac8c88cc6ee6fb2ffdd533ffd24d393becddf011469d4d9ff3040c4f2eb4e37", size = 2234606 }, + { url = "https://files.pythonhosted.org/packages/cf/4d/94ef4eb3a61ef66fc49368eb7687a0cdc97c76b5de538961704c7e92877c/cql2-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:09ac2e157f00a3b33b4cc8cc62d9839162711a0642854bc63d9b98226c95e618", size = 2308880 }, + { url = "https://files.pythonhosted.org/packages/b6/22/276a7fe27ea76ce23f84b034f601b525e65172776f317967912d69ad5772/cql2-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:00e993d8deb355f42f91f408e5460d3b0bd1602aa8fd49b346fc61be44a41e39", size = 2346941 }, + { url = "https://files.pythonhosted.org/packages/80/d9/bc2e4059657cda2edeb6904155206ab3e7b15578a1bfdbde233fcbb2d100/cql2-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:64001c252b02b0260c7854fe8ea0b0b860e0ba966a46a4d74639254ef6088a35", size = 2371826 }, + { url = "https://files.pythonhosted.org/packages/cb/90/d9e9bbdd9b7e68eaadda37d11ab0d37f893a8342dcf7fc52392e63041242/cql2-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef99a58f720bf1564bec025c99177d2ae54c1b56afa3306e27a44f08f5649a9b", size = 2403000 }, + { url = "https://files.pythonhosted.org/packages/db/50/a9842f09b30ddfafc68ff2f38165c10aa07cd61a6b3fc3c3b3b13f479707/cql2-0.3.2-cp311-cp311-win32.whl", hash = "sha256:7eaa81ba79e5fd5044e57aded9b0c241766df861158dac8c899bfc01b8ce8811", size = 1641861 }, + { url = "https://files.pythonhosted.org/packages/9c/c1/8e01c5d1d193d12118529853cd4049c2521d6ba93ca977cad9e626120bcf/cql2-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:637013944b2f49446e1ad659f8d09759cc492c9f24b6cde64ff4c69e7d646436", size = 1781128 }, + { url = "https://files.pythonhosted.org/packages/a2/95/4430e64508b3f29f3ab79723c36c0d03cd0e4751f27fba454f2d4fe28b7f/cql2-0.3.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bbc3ef27950867255bc8e7661cabb20c99b17ea52f9699a913ff0a08c9715a14", size = 2029089 }, + { url = "https://files.pythonhosted.org/packages/fc/3b/98116f959817bff81dc1e992e74a8f0d501c161e01d2c77bf7c8da14764a/cql2-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e556aeae568bf57f0ea67f9287edcc35d76791eb5c6f1223665d14f76c864b6e", size = 1933831 }, + { url = "https://files.pythonhosted.org/packages/e0/43/b2ba8946d70f74e6fea48fac3d51630c1d6b1986b559583598d591005dd5/cql2-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5315d8ff3c4ed34a900cc0254fe976de115a86b473aedc9f23c662a3df3f1e7b", size = 2136153 }, + { url = "https://files.pythonhosted.org/packages/13/13/3f61cd67b4480ca103efbfde75b95b4ae76d9b450bb9b77a89e2d395f2d8/cql2-0.3.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b7d2926f32c128a7eded5c0e10ad5d0744ace6dc348ad67a06b56a071a8ce9e", size = 2084411 }, + { url = "https://files.pythonhosted.org/packages/4f/d6/4280046cfce583d6dd6e07565072083eb265ae11bddb3c5827a97c90ceac/cql2-0.3.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89ce75cbb5e3cee971d3a26923499052aabf8419e1e6791340c69dfce42106df", size = 2299973 }, + { url = "https://files.pythonhosted.org/packages/b4/89/47c030cb8d34928598c17384682dacd65f8dc30cf0e88b70e595c0ef4b86/cql2-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6b0597334668320ed57eabba72bd21807fe34ac11e04c5ac6a0389dbf10336a", size = 2320875 }, + { url = "https://files.pythonhosted.org/packages/3a/b0/f648a1c8bd39d69c949be9a2f8d19771ee14d1ef59f85fae89e131ed9bca/cql2-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d9507574c6637ac06bab2e746cf5a25cd16abed2c268957200b3c6237f6d250", size = 2428137 }, + { url = "https://files.pythonhosted.org/packages/bd/55/22fca91525b765d76b0ac09dfcc53574c7d04d32fd16d3d84462b3bc1f65/cql2-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b64bed33ace7683125b17428381c15d4c3e74b0ca851cce4f59a6309ed3fb5b", size = 2237411 }, + { url = "https://files.pythonhosted.org/packages/ff/3e/67dd15035efb0ee79a54de04a7a980487adb86e1cf6e2a17fe31a37cd48e/cql2-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cdc6fedecc0743e8f6106423135fd0e0dd0148e705259caaa7d3d31b79748115", size = 2310728 }, + { url = "https://files.pythonhosted.org/packages/f8/b1/03d19a7f3830e3ba9d98f8154e34cccd69afb3f1b860a3b73578f7cb79c0/cql2-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:98b49478bd5fa3368b45587e83263bf1cb40b8c37b5610d078d265bbf82f5711", size = 2347134 }, + { url = "https://files.pythonhosted.org/packages/da/0a/d3a3ceb400504a9f61d514b6de1ffe7518d71e6d0355e7ff840e37aebef8/cql2-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b779d1c406aa8caceacfa6a64fda2749e7d38874779578f3d01298e752def8b1", size = 2369587 }, + { url = "https://files.pythonhosted.org/packages/10/b1/b62ac10cdb7c200da6ee0dfee15c11acec1e7b9f0030cbf045190954ca16/cql2-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e732e8cdb9d43bd1e4217c46d89a9819237a22934d4c7dc98e0f7ece0233455", size = 2405759 }, + { url = "https://files.pythonhosted.org/packages/a6/e5/2a462e2d983d1ff8693496e4c8689d13126050026cfcdcadeb38902e4ab6/cql2-0.3.2-cp312-cp312-win32.whl", hash = "sha256:3fa372dec0b8157c64845d7015cf39ababce62864e7c81b6f7faf07e9e7a0372", size = 1641858 }, + { url = "https://files.pythonhosted.org/packages/5b/e5/60197ac1c93eaf84170d58126a65bc6f199df91a20562352dc8a312a30c9/cql2-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:b2f28c63dee08c2cdd13069dcf2b326bfbae5e564a1887ced2fb979afbb367ad", size = 1782488 }, + { url = "https://files.pythonhosted.org/packages/58/16/2fa57b21f288a57efed1cef32139ef67b6b338d04d6a17420b99e5f806ac/cql2-0.3.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2420720b5d5a82cccea826374fd7d0ea53412c8ae9d9f8c356baf857d9ca41ad", size = 2029321 }, + { url = "https://files.pythonhosted.org/packages/5b/cf/14e2c727ec66514aa1236e6917eb6619181f0ebe263fae3a9fdcbc4fc45a/cql2-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1b7f1cac9b5ef01a5f85455490403e8ade461f452bb72d74e0b49ba2c960f1b3", size = 1934570 }, + { url = "https://files.pythonhosted.org/packages/d0/f4/1d118449b0d41745000e6e15f770eec072d7e27d64d7e70c67ffdb0873dc/cql2-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5d3c1a476d67e6152f30c54d7d9e1d6f35639ffc3fdc919fbabe23677c67f63", size = 2136556 }, + { url = "https://files.pythonhosted.org/packages/b2/ab/45f7c553af8eab7bfb18be918f45e5caaa9bfe547c20fadf98179b948cfb/cql2-0.3.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc7cf1ce74c45e1169e84635fed58cedef411aa2aa423a900ffbea00c2f6e4fd", size = 2084705 }, + { url = "https://files.pythonhosted.org/packages/28/eb/6d6de9dd85407927597052d514e323c0205d0deee800a7bd51482d738571/cql2-0.3.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53e769a0c9e2557a525477022efdf53644d45ac3b2d62a959c9bbbab8cea8d62", size = 2300115 }, + { url = "https://files.pythonhosted.org/packages/ce/1d/0684fbe781724edf03728f6a8ec881265caac6e8c748daa18e4fd97c625f/cql2-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8150ffc2d0c8fc3d8d7b6ef54809cd7a5539520f19c2ab388aa2dd2971bfd594", size = 2321275 }, + { url = "https://files.pythonhosted.org/packages/49/17/6253f9fe662922ec3434fa69f41048a97186aabfe9c6be611facdf199c30/cql2-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad283e90eb7b64074771a78bb3b623bf2e9c4a587b852268d8799be237d41b39", size = 2427969 }, + { url = "https://files.pythonhosted.org/packages/3f/89/b71248df31b987c87dbe14adf24a4bfc43f4c4e805b18995672ace0233fd/cql2-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef8d5a42d0ae3ebfb01ea375e598afa6aed31c952a1fded23cdff36245d6b1d0", size = 2238053 }, + { url = "https://files.pythonhosted.org/packages/a8/7a/dcc9d98a64b247ad18857e0499148dda179e08aa1b6bd9f56796c6001e8f/cql2-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8ff0728ab633c5991ca7a1ccbc6ba1a5fa46931093eadc932e3ff5423423fa2", size = 2311483 }, + { url = "https://files.pythonhosted.org/packages/b3/4f/836573be571c09d07b05a133a3264372d74eb390693e936a31e3385aca1e/cql2-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4cd2c9da47c0dd9adf1faa734695840e240813ce9f42c42cedf00c186a1e3211", size = 2347255 }, + { url = "https://files.pythonhosted.org/packages/ca/8f/4dc89195155325dad657e065d39fe8a7b5448bff845faa9390ac2c063702/cql2-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0226121b04d3b721580fe34f36f3d0a5c3c4d1afa316ca128ab1fdae244fa249", size = 2369963 }, + { url = "https://files.pythonhosted.org/packages/41/68/8041a83f6709a04cf88255e5b2f334eb9ebe06cd521153f63aa7265b4b57/cql2-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3421516cb0a53e3c032537df953b60849135bf2d39d5830258dfe526d3715b7", size = 2406731 }, + { url = "https://files.pythonhosted.org/packages/ed/33/20f989d7e389127f51de0961cb79cbdbd96e8117b3dfbde33db5f3cfb9ee/cql2-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e20a313fd3a8d071e550b7f5c25b572783fd69e4b81bad8c108fa1483bf42657", size = 2131638 }, + { url = "https://files.pythonhosted.org/packages/53/99/b05f5046bf0cdeba079c59959c9fa30ea39db3e0350e7b2c30fd45684ca9/cql2-0.3.2-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b3b962fa60cb9a73f3f66faf3a709a392a0d09b2cedfadb59d688ffab01cdba", size = 2082521 }, + { url = "https://files.pythonhosted.org/packages/e2/6a/9e8a9498b42c1e7ad12f83d651f750c6cafee2d4eecaffc0395393ff031a/cql2-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0810e127abbc41b46fa273f0e85eb53445740eb9a1a6dd93ca84a4f24f6e9c0", size = 2317489 }, + { url = "https://files.pythonhosted.org/packages/82/1a/c0c044da2a3608da156e42809f124808a307ce668ff1acb4c57c0379e2df/cql2-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87bd227022f1d4a8e0d6f5c31e25f4cc04505059c70e242499c55d8c2bb6f913", size = 2428349 }, + { url = "https://files.pythonhosted.org/packages/8b/53/fe80aab990666a2c878ab1f4a2ec02a38fa3dc9b348324468f89dcbfa0f0/cql2-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0372c724905932f25d3f206831cb620725d408c12da2d3cb83952a4d58ad2499", size = 2307038 }, + { url = "https://files.pythonhosted.org/packages/0b/eb/819eda4a075a1776deef647186227fec8b01763eeace4add764e5983c006/cql2-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e569f3a4bd93b2ede1057165abb6167df3a046c30fafee1f95ac652ab6359724", size = 2344364 }, + { url = "https://files.pythonhosted.org/packages/e9/6a/3fe76ec2dfd88c60e8de429cfcbca4e71229acebd22f6b70aaa1a1b9e2a0/cql2-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e56bd223c899a647ffceb93f2fbe54915247a31325d48d7928206ac7e47c5c22", size = 2369238 }, + { url = "https://files.pythonhosted.org/packages/07/cd/3987aca1778e6df4ddef6d0e0ecca43d820d3f2b0a1798ae0fc881b514e9/cql2-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b18885b1c48275db1c2b03fab1701a6933e4368dc2b39123ad48d80d748f0e72", size = 2403030 }, + { url = "https://files.pythonhosted.org/packages/ff/bf/4d936280e370e75d0bbdd70dcfe69b50db37e6677b3b1528b7c6db0e6d45/cql2-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bac9392f1eb12660f1119446e0437e62cf0dac5006d5f567ae2d2309fccd08f", size = 2135608 }, + { url = "https://files.pythonhosted.org/packages/34/aa/5351903d265a9682f23a6fb0ebe0b0bf0464c059673a0c846d81f8180f76/cql2-0.3.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:292c8ea8a21bf4d21a8cf69ed7a952729eb1b2df6f04973b560c031092769eac", size = 2084844 }, + { url = "https://files.pythonhosted.org/packages/ac/86/ee9cd8eb0814fbf7f38067652b56a8a4f0efc133282f8f99e3a6bb27d647/cql2-0.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddcb38737db6b521864fe746d02e5428e3c4f19c1e34455bcc4444eef89fe51d", size = 2303534 }, + { url = "https://files.pythonhosted.org/packages/e1/05/7306789d47b10bb9b6e82eb7c3181b7fee91beff0481adb4910c6f54abfa/cql2-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636a009439a2d440a4f68842db1b3f741f44c1206bd70a0c3148fb904b1f3a29", size = 2319094 }, + { url = "https://files.pythonhosted.org/packages/d3/12/7346ecea9e0bb059b50313f5c117d3ce321cdbe2529f808dfbe6572d008f/cql2-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:555d28efc5a330c4e74436fd8aeb4449efe93bf7592841db304f23fb2c00116f", size = 2428854 }, + { url = "https://files.pythonhosted.org/packages/8e/d0/41f967dd99b989068d40c9eee13808b2cf65e33ca6e81e205beabd39bea3/cql2-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3be74543147539df5b80576bc3e08ed187ddf74e2dd26c584b51d437487e25aa", size = 2235564 }, + { url = "https://files.pythonhosted.org/packages/c1/b1/c1d6587ef96fe2c8a5e580cde07514df96a62bd74057841e923d4ccef24f/cql2-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:afda43b2d6bc6c39a49da4cc19fae384c04ff14c29de2a52c4e6ba3c7741c00c", size = 2310564 }, + { url = "https://files.pythonhosted.org/packages/41/c4/fc773a208ad8b27b81eeb05bceb852084b1e52ce14ad11cdca4344d1534a/cql2-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:946464b4bd1fccad0cd5534144eba5bc44396e3cec7d8f7420d7587b99965262", size = 2347518 }, + { url = "https://files.pythonhosted.org/packages/f7/d1/e758c332b15c1cf8e3b3cefb33ae9faee7d7e966b2701d20ea1eb8d8834a/cql2-0.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:b0c39f0dc81a218aeb47b265cde91173f5f98e288e0f12790cffe2799df2cd72", size = 2372620 }, + { url = "https://files.pythonhosted.org/packages/26/ca/1eb4088a5affc1fb7b1cac96256dec212432f5c49c408e179c9bed37c179/cql2-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98f423ee4dcdd502aa0a4f2dadb6ebefdc1b600e1b71ca0f0d1a17774ba50657", size = 2404337 }, + { url = "https://files.pythonhosted.org/packages/d9/db/c67381019c1d16055ea4cf83b0e5abd26a0ad08581d0fc18c9a02af69b56/cql2-0.3.2-cp39-cp39-win32.whl", hash = "sha256:73a1dbcf69487850fdd954e61fa822b50e59f61e9a428e8baf023414df46f985", size = 1642591 }, + { url = "https://files.pythonhosted.org/packages/b2/da/9fbd4f655ece64d5537ccb352f97edc90c844a8b335f99b9ad3f3e5f0ec4/cql2-0.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:0fd3331304f5ab983eb3c07a688cc2ba68a80dd7ef1a04143f5ebcc20407e69a", size = 1781686 }, + { url = "https://files.pythonhosted.org/packages/2d/e6/68acc4adfdca78224335e59af7b6e69430f7d723e24893424f848b0ed5b3/cql2-0.3.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1303669abe9feba5337d53d7c89346ea27e38e7bb84525e76c90a1e71145df5a", size = 2135716 }, + { url = "https://files.pythonhosted.org/packages/77/28/4f823fcc21e860cb91fcf8ff0f47892e0eaffe3c908d717fb1c7403bdde0/cql2-0.3.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a7cf27aeaf668345a6ad7c804d6784e5e77a839646e2b8ff3d55816a758ec785", size = 2085503 }, + { url = "https://files.pythonhosted.org/packages/8d/cf/32f23f837c84086765a16c91c6f47b714fc9fa9cb022d9a5a2a2ec6131f0/cql2-0.3.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:217ddb0926781cd6d54034e77cd84485eef926baf371b53cd3368bf5a1e71cf4", size = 2302221 }, + { url = "https://files.pythonhosted.org/packages/09/f3/a31637510d0ad508e9266c58e5f820c8b37ab1c73d3c2d03b1beee02ce13/cql2-0.3.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c1a04f6491443911b3a36820742e6d9ee52dbd20642fe8fdff8aff157bdcdab", size = 2319491 }, + { url = "https://files.pythonhosted.org/packages/05/7e/8b06eed71017421864ef5f5a738d5812def0630ce53e26d65070be66275f/cql2-0.3.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:137122747663918065508f2a0305d3845dca67f55d00669968460e6ceb0ae120", size = 2428927 }, + { url = "https://files.pythonhosted.org/packages/c4/cd/2506a35fca318aa19d4f6005435ead548e10e4edcc0c8d080e8450925f88/cql2-0.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce928eef7f8f5686c0b5f2de634ca29d22bca2c8f6e8c0a23a44ee8a18da84b", size = 2235706 }, + { url = "https://files.pythonhosted.org/packages/e5/03/f3e05e2079d1d70712af78e4c56e6c6795e7b7eee9546f4d94e914cd597d/cql2-0.3.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:60d3946be4f9f5182ef4a18346b37f5f96007b1201b3c161726c4f5a064b9a81", size = 2310377 }, + { url = "https://files.pythonhosted.org/packages/45/00/e56c3239461801bfb803759fc5eb6f3287d1fdbe78db463b13f8effaf4a3/cql2-0.3.2-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:0212df0c522b238cb01c7f6bb4a0c88fc281362c47a78bacbdced7aba992bd12", size = 2348062 }, + { url = "https://files.pythonhosted.org/packages/da/8d/e3f665bffa29b8470cb2251fca5d5037ae9a8fbbfc2f430e42607bc0b87d/cql2-0.3.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:9ae9239fcb52dc15d03cc0977e68c7975f42ce1813cf386e0dc4ba8410684f5d", size = 2370345 }, + { url = "https://files.pythonhosted.org/packages/de/94/1ec6180aa95924b4e00134d551f8abd5ca872275a68f58ff48f4b24df9d6/cql2-0.3.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:502ec804409540ee5abd928a36491e33b82b700e07e46534ff885d46e026ed32", size = 2404446 }, + { url = "https://files.pythonhosted.org/packages/b2/4e/ff3eed27ef0db17fb2f3f73ee1cca637516b45b4feda724b902245900d5e/cql2-0.3.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95a90a2ad95927eec8aceb3450cfa9f30a2432d37b96c0ca4401cee38f26c296", size = 2135285 }, + { url = "https://files.pythonhosted.org/packages/a0/7c/1e8a88ba8c8ae96847bec3abc4f512adade1be6e5080a96baad89d557bf0/cql2-0.3.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e59715c05e2e30611e8260d59a65284795ad3d3db3e05ac37c341e4a57fd8656", size = 2085230 }, + { url = "https://files.pythonhosted.org/packages/e4/88/7bddcf5f1d7809436e53460cbe8bce9e1aef04d1a8eadbb39f334906a085/cql2-0.3.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c0472c38796544da579a3b685c81c8c6f0200918dc66de52279d6a29db864f37", size = 2319433 }, + { url = "https://files.pythonhosted.org/packages/3e/22/660d6319a8ec2fa32866df2cd820772654e8dfe5a5308441d1c9e7b730d7/cql2-0.3.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a4eef8d7ece85851a0152453e93555c6b74cfe2b56807a70cb1e7018779433", size = 2428647 }, + { url = "https://files.pythonhosted.org/packages/55/2e/c4a6571139f095d281cb2a6f112f187418f42d02667646974fbd5a7402c7/cql2-0.3.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:313f35ca16e4942303be2d934bb1608e7503e83ab3a7fc67e5ffd7092e33133c", size = 2309672 }, + { url = "https://files.pythonhosted.org/packages/14/f1/4c047a2fce51cd18454dd5d079a9227c5b7c923a42096b1b1fc1a76a8d50/cql2-0.3.2-pp39-pypy39_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:863cbde1e67cd41f5a2c1fa63ca4b253193ccf350dc722efa6d38dc1eaefcba7", size = 2348148 }, + { url = "https://files.pythonhosted.org/packages/bc/df/1dd599b88ff990fa41c5717f71edc9d71cc4fc2985f4ddd52923023e0ba9/cql2-0.3.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:5b5adfcda6ca19aebbc04f715a0dfea92289750dd6555c2cf0e39de2e4272a3c", size = 2371068 }, + { url = "https://files.pythonhosted.org/packages/fe/56/66d2dfff1fb6c4d87ecbc5cfd77011be2a47914ca126bb410deda0a45180/cql2-0.3.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ad18d79ba4d09823f9718e4a123e1ed6aca3119e26a15912c8f5a7546555cbba", size = 2404934 }, +] + [[package]] name = "cryptography" version = "44.0.0" @@ -486,7 +418,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 }, { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 }, { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 }, - { url = "https://files.pythonhosted.org/packages/4e/d5/9cc182bf24c86f542129565976c21301d4ac397e74bf5a16e48241aab8a6/cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385", size = 4164756 }, { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 }, { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 }, { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 }, @@ -497,7 +428,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 }, { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 }, { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 }, - { url = "https://files.pythonhosted.org/packages/31/d9/90409720277f88eb3ab72f9a32bfa54acdd97e94225df699e7713e850bd4/cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba", size = 4165207 }, { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 }, { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 }, { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 }, @@ -519,20 +449,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, ] -[[package]] -name = "eoapi-auth-utils" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "fastapi" }, - { name = "pyjwt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/cc/185f9d7c42a124b0405cf9ea4def38659a13cf47d28a657b49b7407cebfb/eoapi.auth-utils-0.4.0.tar.gz", hash = "sha256:621abe10eed293a5a2bc158a62d25311aa4dfced0bbf4a2c6ee8aefc443bb3aa", size = 5180 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/ac/877ebdeb4ca4d16d34936a3a72cb9544271adaf64760a243a81becc037ab/eoapi.auth_utils-0.4.0-py3-none-any.whl", hash = "sha256:aa55e5c20012796603bb09740b3689ffc31b91df470a43c7765ae37e6c68f276", size = 6766 }, -] - [[package]] name = "exceptiongroup" version = "1.2.2" @@ -630,12 +546,15 @@ wheels = [ ] [[package]] -name = "jmespath" -version = "1.0.1" +name = "jinja2" +version = "3.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, ] [[package]] @@ -652,12 +571,71 @@ wheels = [ ] [[package]] -name = "lark-parser" -version = "0.12.0" +name = "markupsafe" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/ee/fd1192d7724419ddfe15b6f17d1c8742800d4de917c0adac3b6aaf22e921/lark-parser-0.12.0.tar.gz", hash = "sha256:15967db1f1214013dca65b1180745047b9be457d73da224fcda3d9dd4e96a138", size = 235029 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/00/90f05db333fe1aa6b6ffea83a35425b7d53ea95c8bba0b1597f226cf1d5f/lark_parser-0.12.0-py2.py3-none-any.whl", hash = "sha256:0eaf30cb5ba787fe404d73a7d6e61df97b21d5a63ac26c5008c78a494373c675", size = 103498 }, + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, ] [[package]] @@ -799,19 +777,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, - { url = "https://files.pythonhosted.org/packages/97/bb/c62074a65a32ed279bef44862e89fabb5ab1a81df8a9d383bddb4f49a1e0/pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62", size = 1901535 }, - { url = "https://files.pythonhosted.org/packages/9b/59/e224c93f95ffd4f5d37f1d148c569eda8ae23446ab8daf3a211ac0533e08/pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab", size = 1781287 }, - { url = "https://files.pythonhosted.org/packages/11/e2/33629134e577543b9335c5ca9bbfd2348f5023fda956737777a7a3b86788/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864", size = 1834575 }, - { url = "https://files.pythonhosted.org/packages/fe/16/82e0849b3c6deb0330c07f1a8d55708d003ec8b1fd38ac84c7a830e25252/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067", size = 1857948 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/cdee588a7440bc58b6351e8b8dc2432e38b1144b5ae6625bfbdfb7fa76d9/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd", size = 2041138 }, - { url = "https://files.pythonhosted.org/packages/1d/0e/73e0d1dff37a29c31e5b3e8587d228ced736cc7af9f81f6d7d06aa47576c/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5", size = 2783820 }, - { url = "https://files.pythonhosted.org/packages/9a/b1/f164d05be347b99b91327ea9dd1118562951d2c86e1ea943ef73636b0810/pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78", size = 2138035 }, - { url = "https://files.pythonhosted.org/packages/72/44/cf1f20d3036d7e1545eafde0af4f3172075573a407a3a20313115c8990ff/pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f", size = 1991778 }, - { url = "https://files.pythonhosted.org/packages/5d/4c/486d8ddd595892e7d791f26dfd3e51bd8abea478eb7747fe2bbe890a2177/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36", size = 1996644 }, - { url = "https://files.pythonhosted.org/packages/33/2a/9a1cd4c8aca242816be431583a3250797f2932fad32d35ad5aefcea179bc/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a", size = 2091778 }, - { url = "https://files.pythonhosted.org/packages/8f/61/03576dac806c49e76a714c23f501420b0aeee80f97b995fc4b28fe63a010/pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b", size = 2146020 }, - { url = "https://files.pythonhosted.org/packages/72/82/e236d762052d24949aabad3952bc2c8635a470d6f3cbdd69498692afa679/pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618", size = 1819443 }, - { url = "https://files.pythonhosted.org/packages/6e/89/26816cad528ca5d4af9be33aa91507504c4576100e53b371b5bc6d3c797b/pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4", size = 1979478 }, { url = "https://files.pythonhosted.org/packages/bc/6a/d741ce0c7da75ce9b394636a406aace00ad992ae417935ef2ad2e67fb970/pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967", size = 1898376 }, { url = "https://files.pythonhosted.org/packages/bd/68/6ba18e30f10c7051bc55f1dffeadbee51454b381c91846104892a6d3b9cd/pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60", size = 1777246 }, { url = "https://files.pythonhosted.org/packages/36/b8/6f1b7c5f068c00dfe179b8762bc1d32c75c0e9f62c9372174b1b64a74aa8/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854", size = 1832148 }, @@ -860,11 +825,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.9.0" +version = "2.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", size = 78825 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", size = 22344 }, + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, ] [[package]] @@ -897,18 +862,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, ] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, -] - [[package]] name = "python-dotenv" version = "1.0.1" @@ -918,15 +871,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, ] -[[package]] -name = "pytz" -version = "2024.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, -] - [[package]] name = "pyyaml" version = "6.0.2" @@ -969,13 +913,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, - { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218 }, - { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067 }, - { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812 }, - { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531 }, - { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820 }, - { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514 }, - { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702 }, { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, @@ -987,30 +924,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, ] -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -1027,11 +940,12 @@ source = { editable = "." } dependencies = [ { name = "authlib" }, { name = "brotli" }, - { name = "cel-python" }, - { name = "eoapi-auth-utils" }, + { name = "cql2" }, { name = "fastapi" }, { name = "httpx" }, + { name = "jinja2" }, { name = "pydantic-settings" }, + { name = "pyjwt" }, { name = "uvicorn" }, ] @@ -1047,11 +961,12 @@ dev = [ requires-dist = [ { name = "authlib", specifier = ">=1.3.2" }, { name = "brotli", specifier = ">=1.1.0" }, - { name = "cel-python", specifier = ">=0.1.5" }, - { name = "eoapi-auth-utils", specifier = ">=0.4.0" }, + { name = "cql2", specifier = ">=0.3.2" }, { name = "fastapi", specifier = ">=0.115.5" }, { name = "httpx", specifier = ">=0.28.0" }, + { name = "jinja2", specifier = ">=3.1.4" }, { name = "pydantic-settings", specifier = ">=2.6.1" }, + { name = "pyjwt", specifier = ">=2.10.1" }, { name = "uvicorn", specifier = ">=0.32.1" }, ] @@ -1124,15 +1039,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] -[[package]] -name = "urllib3" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, -] - [[package]] name = "uvicorn" version = "0.32.1" From cdd404078ce4ee404a8c2ee08d6d4b8df42c608a Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Wed, 11 Dec 2024 23:10:22 -0800 Subject: [PATCH 05/29] Refactor config --- src/stac_auth_proxy/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index 8ea16f6..7f3694b 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -3,7 +3,7 @@ import importlib from typing import Optional, Sequence, TypeAlias -from pydantic import BaseModel +from pydantic import BaseModel, Field from pydantic.networks import HttpUrl from pydantic_settings import BaseSettings, SettingsConfigDict @@ -14,8 +14,8 @@ class ClassInput(BaseModel): """Input model for dynamically loading a class or function.""" cls: str - args: Optional[Sequence[str]] = [] - kwargs: Optional[dict[str, str]] = {} + args: Optional[Sequence[str]] = Field(default_factory=list) + kwargs: Optional[dict[str, str]] = Field(default_factory=dict) def __call__(self, token_dependency): """Dynamically load a class and instantiate it with kwargs.""" From 902a8bb56b9ed48552386aa340df18b57d96fa50 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 12 Dec 2024 09:04:19 -0800 Subject: [PATCH 06/29] fix: correct auth handler --- src/stac_auth_proxy/auth.py | 127 ++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 65 deletions(-) diff --git a/src/stac_auth_proxy/auth.py b/src/stac_auth_proxy/auth.py index d46d9d2..2a7e5b2 100644 --- a/src/stac_auth_proxy/auth.py +++ b/src/stac_auth_proxy/auth.py @@ -7,10 +7,9 @@ import jwt from fastapi import HTTPException, Security, security, status from fastapi.security.base import SecurityBase +from pydantic import AnyHttpUrl from starlette.exceptions import HTTPException from starlette.status import HTTP_403_FORBIDDEN -from pydantic import AnyHttpUrl - logger = logging.getLogger(__name__) @@ -42,75 +41,73 @@ def __post_init__(self): oidc_config = json.load(response) self.jwks_client = jwt.PyJWKClient(oidc_config["jwks_uri"]) - self.valid_token_dependency.__annotations__["auth_header"] = ( - security.OpenIdConnect( - openIdConnectUrl=str(self.openid_configuration_url), auto_error=True - ) + self.auth_scheme = security.OpenIdConnect( + openIdConnectUrl=str(self.openid_configuration_url), + auto_error=False, ) - - def user_or_none(self, auth_header: Annotated[str, Security(auth_scheme)]): - """Return the validated user if authenticated, else None.""" - return self.valid_token_dependency( - auth_header, security.SecurityScopes([]), auto_error=False - ) - - def valid_token_dependency( - self, - auth_header: Annotated[str, Security(auth_scheme)], - required_scopes: security.SecurityScopes, - auto_error: bool = True, - ): - """Dependency to validate an OIDC token.""" - if not auth_header: - if auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) - return None - - # Extract token from header - token_parts = auth_header.split(" ") - if len(token_parts) != 2 or token_parts[0].lower() != "bearer": - logger.error(f"Invalid token: {auth_header}") - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - [_, token] = token_parts - - # Parse & validate token - try: - key = self.jwks_client.get_signing_key_from_jwt(token).key - payload = jwt.decode( - token, - key, - algorithms=["RS256"], - # NOTE: Audience validation MUST match audience claim if set in token (https://pyjwt.readthedocs.io/en/stable/changelog.html?highlight=audience#id40) - audience=self.allowed_jwt_audiences, - ) - except (jwt.exceptions.InvalidTokenError, jwt.exceptions.DecodeError) as e: - logger.exception(f"InvalidTokenError: {e=}") - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) from e - - # Validate scopes (if required) - for scope in required_scopes.scopes: - if scope not in payload["scope"]: + self.user_or_none = self.build(auto_error=False) + self.valid_token_dependency = self.build(auto_error=True) + + def build(self, auto_error: bool = True): + """Build a dependency for validating an OIDC token.""" + + def valid_token_dependency( + auth_header: Annotated[str, Security(self.auth_scheme)], + required_scopes: security.SecurityScopes, + ): + """Dependency to validate an OIDC token.""" + if not auth_header: if auto_error: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not enough permissions", - headers={ - "WWW-Authenticate": f'Bearer scope="{required_scopes.scope_str}"' - }, + status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" ) return None - return payload + # Extract token from header + token_parts = auth_header.split(" ") + if len(token_parts) != 2 or token_parts[0].lower() != "bearer": + logger.error(f"Invalid token: {auth_header}") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + [_, token] = token_parts + + # Parse & validate token + try: + key = self.jwks_client.get_signing_key_from_jwt(token).key + payload = jwt.decode( + token, + key, + algorithms=["RS256"], + # NOTE: Audience validation MUST match audience claim if set in token (https://pyjwt.readthedocs.io/en/stable/changelog.html?highlight=audience#id40) + audience=self.allowed_jwt_audiences, + ) + except (jwt.exceptions.InvalidTokenError, jwt.exceptions.DecodeError) as e: + logger.exception(f"InvalidTokenError: {e=}") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) from e + + # Validate scopes (if required) + for scope in required_scopes.scopes: + if scope not in payload["scope"]: + if auto_error: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={ + "WWW-Authenticate": f'Bearer scope="{required_scopes.scope_str}"' + }, + ) + return None + + return payload + + return valid_token_dependency class OidcFetchError(Exception): From 29b480625fe77174b3528f9dd42dc6245bf0aca9 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 12 Dec 2024 09:43:20 -0800 Subject: [PATCH 07/29] Lint cleanup --- src/stac_auth_proxy/auth.py | 34 +++++++++++++++---------- src/stac_auth_proxy/config.py | 9 +++---- src/stac_auth_proxy/filters/__init__.py | 2 ++ src/stac_auth_proxy/filters/template.py | 11 +++++--- tests/test_openapi.py | 2 +- 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/stac_auth_proxy/auth.py b/src/stac_auth_proxy/auth.py index 2a7e5b2..3eab155 100644 --- a/src/stac_auth_proxy/auth.py +++ b/src/stac_auth_proxy/auth.py @@ -1,3 +1,5 @@ +"""OIDC authentication module for validating JWTs.""" + import json import logging import urllib.request @@ -7,29 +9,32 @@ import jwt from fastapi import HTTPException, Security, security, status from fastapi.security.base import SecurityBase -from pydantic import AnyHttpUrl -from starlette.exceptions import HTTPException -from starlette.status import HTTP_403_FORBIDDEN +from pydantic import HttpUrl logger = logging.getLogger(__name__) @dataclass class OpenIdConnectAuth: - openid_configuration_url: AnyHttpUrl - openid_configuration_internal_url: Optional[AnyHttpUrl] = None + """OIDC authentication class to generate auth handlers.""" + + openid_configuration_url: HttpUrl + openid_configuration_internal_url: Optional[HttpUrl] = None allowed_jwt_audiences: Optional[Sequence[str]] = None # Generated attributes auth_scheme: SecurityBase = field(init=False) jwks_client: jwt.PyJWKClient = field(init=False) - valid_token_dependency: Callable[..., Any] = field(init=False) + validated_user: Callable[..., Any] = field(init=False) + maybe_validated_user: Callable[..., Any] = field(init=False) def __post_init__(self): + """Initialize the OIDC authentication class.""" logger.debug("Requesting OIDC config") - with urllib.request.urlopen( - str(self.openid_configuration_internal_url or self.openid_configuration_url) - ) as response: + origin_url = ( + self.openid_configuration_internal_url or self.openid_configuration_url + ) + with urllib.request.urlopen(origin_url) as response: if response.status != 200: logger.error( "Received a non-200 response when fetching OIDC config: %s", @@ -45,10 +50,10 @@ def __post_init__(self): openIdConnectUrl=str(self.openid_configuration_url), auto_error=False, ) - self.user_or_none = self.build(auto_error=False) - self.valid_token_dependency = self.build(auto_error=True) + self.validated_user = self._build(auto_error=True) + self.maybe_validated_user = self._build(auto_error=False) - def build(self, auto_error: bool = True): + def _build(self, auto_error: bool = True): """Build a dependency for validating an OIDC token.""" def valid_token_dependency( @@ -59,7 +64,8 @@ def valid_token_dependency( if not auth_header: if auto_error: raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" + status_code=status.HTTP_403_FORBIDDEN, + detail="Not authenticated", ) return None @@ -111,4 +117,6 @@ def valid_token_dependency( class OidcFetchError(Exception): + """Error fetching OIDC configuration.""" + pass diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index 7f3694b..19f77d2 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -14,8 +14,8 @@ class ClassInput(BaseModel): """Input model for dynamically loading a class or function.""" cls: str - args: Optional[Sequence[str]] = Field(default_factory=list) - kwargs: Optional[dict[str, str]] = Field(default_factory=dict) + args: Sequence[str] = Field(default_factory=list) + kwargs: dict[str, str] = Field(default_factory=dict) def __call__(self, token_dependency): """Dynamically load a class and instantiate it with kwargs.""" @@ -48,10 +48,7 @@ class Settings(BaseSettings): public_endpoints: EndpointMethods = {"/api.html": ["GET"], "/api": ["GET"]} openapi_spec_endpoint: Optional[str] = None - collections_filter: Optional[ClassInput] = { - "cls": "stac_auth_proxy.filters.Template", - "args": ["""A_CONTAINEDBY(id, ( '{{ token.collections | join("', '") }}' ))"""], - } + collections_filter: Optional[ClassInput] = None items_filter: Optional[ClassInput] = None model_config = SettingsConfigDict(env_prefix="STAC_AUTH_PROXY_") diff --git a/src/stac_auth_proxy/filters/__init__.py b/src/stac_auth_proxy/filters/__init__.py index 35f216f..5f2833c 100644 --- a/src/stac_auth_proxy/filters/__init__.py +++ b/src/stac_auth_proxy/filters/__init__.py @@ -1,3 +1,5 @@ +"""CQL2 filter generators.""" + from .template import Template __all__ = ["Template"] diff --git a/src/stac_auth_proxy/filters/template.py b/src/stac_auth_proxy/filters/template.py index ffaea42..3e9a18e 100644 --- a/src/stac_auth_proxy/filters/template.py +++ b/src/stac_auth_proxy/filters/template.py @@ -1,16 +1,19 @@ +"""Generate CQL2 filter expressions via Jinja2 templating.""" + +from dataclasses import dataclass, field from typing import Any, Callable from cql2 import Expr -from jinja2 import Environment, BaseLoader from fastapi import Request, Security +from jinja2 import BaseLoader, Environment from ..utils import extract_variables -from dataclasses import dataclass, field - @dataclass class Template: + """Generate CQL2 filter expressions via Jinja2 templating.""" + template_str: str token_dependency: Callable[..., Any] @@ -18,10 +21,12 @@ class Template: env: Environment = field(init=False) def __post_init__(self): + """Initialize the Jinja2 environment.""" self.env = Environment(loader=BaseLoader).from_string(self.template_str) self.render.__annotations__["auth_token"] = Security(self.token_dependency) async def cql2(self, request: Request, auth_token=Security(...)) -> Expr: + """Render a CQL2 filter expression with the request and auth token.""" # TODO: How to handle the case where auth_token is null? context = { "req": { diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 071e7de..e738ab9 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -40,7 +40,7 @@ def test_no_private_endpoints(source_api_server): assert "info" in openapi assert "openapi" in openapi assert "paths" in openapi - assert "oidcAuth" not in openapi.get("components", {}).get("securitySchemes", {}) + # assert "oidcAuth" not in openapi.get("components", {}).get("securitySchemes", {}) def test_oidc_in_openapi_spec(source_api: FastAPI, source_api_server: str): From 475f95a2f45a60650017f3d6b0edf73f03440268 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 12 Dec 2024 18:57:58 -0800 Subject: [PATCH 08/29] Crude working collections filter --- src/stac_auth_proxy/app.py | 53 +++++++++++++++--- src/stac_auth_proxy/auth.py | 2 +- src/stac_auth_proxy/filters/template.py | 54 ++++++++++--------- src/stac_auth_proxy/handlers/reverse_proxy.py | 50 ++++++++++++++--- src/stac_auth_proxy/utils.py | 47 ++++++++++++++++ 5 files changed, 168 insertions(+), 38 deletions(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index faaa7c7..c5df655 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -6,14 +6,16 @@ """ import logging -from typing import Optional +from typing import Optional, Annotated -from fastapi import Depends, FastAPI +from fastapi import FastAPI, Security, Request, Depends +from cql2 import Expr from .auth import OpenIdConnectAuth from .config import Settings from .handlers import OpenApiSpecHandler, ReverseProxyHandler from .middleware import AddProcessTimeHeaderMiddleware +from .utils import apply_filter logger = logging.getLogger(__name__) @@ -28,8 +30,8 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: app.add_middleware(AddProcessTimeHeaderMiddleware) auth_scheme = OpenIdConnectAuth( - openid_configuration_url=str(settings.oidc_discovery_url) - ).valid_token_dependency + openid_configuration_url=settings.oidc_discovery_url + ) if settings.debug: app.add_api_route( @@ -38,12 +40,40 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: methods=["GET"], ) - proxy_handler = ReverseProxyHandler(upstream=str(settings.upstream_url)) + collections_filter = ( + settings.collections_filter(auth_scheme.maybe_validated_user) + if settings.collections_filter + else None + ) + items_filter = ( + settings.items_filter(auth_scheme.maybe_validated_user) + if settings.items_filter + else None + ) + proxy_handler = ReverseProxyHandler( + upstream=str(settings.upstream_url), + collections_filter=collections_filter, + items_filter=items_filter, + ) openapi_handler = OpenApiSpecHandler( proxy=proxy_handler, oidc_config_url=str(settings.oidc_discovery_url), ) + # @app.get("/collections") + # async def collections( + # request: Request, + # filter: Annotated[Optional[Expr], Depends(collections_filter.dependency)], + # ): + # # if filter: + # # print(f"{request.receive=}") + # # request = await apply_filter( + # # request, + # # filter, + # # ) + # # print(f"{request.receive=}") + # return await proxy_handler.stream(request=request) + # Endpoints that are explicitely marked private for path, methods in settings.private_endpoints.items(): app.add_api_route( @@ -54,7 +84,7 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: else openapi_handler.dispatch ), methods=methods, - dependencies=[Depends(auth_scheme)], + dependencies=[Security(auth_scheme.validated_user)], ) # Endpoints that are explicitely marked as public @@ -67,6 +97,7 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: else openapi_handler.dispatch ), methods=methods, + dependencies=[Security(auth_scheme.maybe_validated_user)], ) # Catchall for remainder of the endpoints @@ -74,7 +105,15 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: "/{path:path}", proxy_handler.stream, methods=["GET", "POST", "PUT", "PATCH", "DELETE"], - dependencies=([] if settings.default_public else [Depends(auth_scheme)]), + dependencies=( + [ + Security( + auth_scheme.maybe_validated_user + if settings.default_public + else auth_scheme.validated_user + ) + ] + ), ) return app diff --git a/src/stac_auth_proxy/auth.py b/src/stac_auth_proxy/auth.py index 3eab155..1e4826e 100644 --- a/src/stac_auth_proxy/auth.py +++ b/src/stac_auth_proxy/auth.py @@ -31,7 +31,7 @@ class OpenIdConnectAuth: def __post_init__(self): """Initialize the OIDC authentication class.""" logger.debug("Requesting OIDC config") - origin_url = ( + origin_url = str( self.openid_configuration_internal_url or self.openid_configuration_url ) with urllib.request.urlopen(origin_url) as response: diff --git a/src/stac_auth_proxy/filters/template.py b/src/stac_auth_proxy/filters/template.py index 3e9a18e..2499c2d 100644 --- a/src/stac_auth_proxy/filters/template.py +++ b/src/stac_auth_proxy/filters/template.py @@ -19,31 +19,37 @@ class Template: # Generated attributes env: Environment = field(init=False) + dependency: Callable[[Request, Security], Expr] = field(init=False) def __post_init__(self): """Initialize the Jinja2 environment.""" self.env = Environment(loader=BaseLoader).from_string(self.template_str) - self.render.__annotations__["auth_token"] = Security(self.token_dependency) - - async def cql2(self, request: Request, auth_token=Security(...)) -> Expr: - """Render a CQL2 filter expression with the request and auth token.""" - # TODO: How to handle the case where auth_token is null? - context = { - "req": { - "path": request.url.path, - "method": request.method, - "query_params": dict(request.query_params), - "path_params": extract_variables(request.url.path), - "headers": dict(request.headers), - "body": ( - await request.json() - if request.headers.get("content-type") == "application/json" - else (await request.body()).decode() - ), - }, - "token": auth_token, - } - cql2_str = self.env.render(**context) - cql2_expr = Expr(cql2_str) - cql2_expr.validate() - return cql2_expr + self.dependency = self.build() + + def build(self): + async def dependency( + request: Request, auth_token=Security(self.token_dependency) + ) -> Expr: + """Render a CQL2 filter expression with the request and auth token.""" + # TODO: How to handle the case where auth_token is null? + context = { + "req": { + "path": request.url.path, + "method": request.method, + "query_params": dict(request.query_params), + "path_params": extract_variables(request.url.path), + "headers": dict(request.headers), + "body": ( + await request.json() + if request.headers.get("content-type") == "application/json" + else (await request.body()).decode() + ), + }, + "token": auth_token, + } + cql2_str = self.env.render(**context) + cql2_expr = Expr(cql2_str) + cql2_expr.validate() + return cql2_expr + + return dependency diff --git a/src/stac_auth_proxy/handlers/reverse_proxy.py b/src/stac_auth_proxy/handlers/reverse_proxy.py index 3ab5377..4416985 100644 --- a/src/stac_auth_proxy/handlers/reverse_proxy.py +++ b/src/stac_auth_proxy/handlers/reverse_proxy.py @@ -3,13 +3,17 @@ import logging import time from dataclasses import dataclass +from typing import Optional, Annotated +from cql2 import Expr import httpx -from fastapi import Request +from fastapi import Request, Depends from starlette.background import BackgroundTask from starlette.datastructures import MutableHeaders from starlette.responses import StreamingResponse +from ..utils import update_qs + logger = logging.getLogger(__name__) @@ -19,6 +23,8 @@ class ReverseProxyHandler: upstream: str client: httpx.AsyncClient = None + collections_filter: Optional[callable] = None + items_filter: Optional[callable] = None def __post_init__(self): """Initialize the HTTP client.""" @@ -27,17 +33,41 @@ def __post_init__(self): timeout=httpx.Timeout(timeout=15.0), ) - async def proxy_request(self, request: Request, *, stream=False) -> httpx.Response: + self.proxy_request.__annotations__["collections_filter"] = Annotated[ + Optional[Expr], Depends(self.collections_filter.dependency) + ] + self.stream.__annotations__["collections_filter"] = Annotated[ + Optional[Expr], Depends(self.collections_filter.dependency) + ] + + async def proxy_request( + self, + request: Request, + *, + collections_filter: Annotated[Optional[Expr], Depends(...)], + stream=False, + ) -> httpx.Response: """Proxy a request to the upstream STAC API.""" headers = MutableHeaders(request.headers) headers.setdefault("X-Forwarded-For", request.client.host) headers.setdefault("X-Forwarded-Host", request.url.hostname) + path = request.url.path + query = request.url.query.encode("utf-8") + # https://github.com/fastapi/fastapi/discussions/7382#discussioncomment-5136466 + # TODO: Examine filters + if collections_filter: + if request.method == "GET" and path == "/collections": + query += b"&" + update_qs( + request.query_params, filter=collections_filter.to_text() + ) + url = httpx.URL( - path=request.url.path, - query=request.url.query.encode("utf-8"), + path=path, + query=query, ) + rp_req = self.client.build_request( request.method, url=url, @@ -56,9 +86,17 @@ async def proxy_request(self, request: Request, *, stream=False) -> httpx.Respon rp_resp.headers["X-Upstream-Time"] = f"{proxy_time:.3f}" return rp_resp - async def stream(self, request: Request) -> StreamingResponse: + async def stream( + self, + request: Request, + collections_filter: Annotated[Optional[Expr], Depends(...)], + ) -> StreamingResponse: """Transparently proxy a request to the upstream STAC API.""" - rp_resp = await self.proxy_request(request, stream=True) + rp_resp = await self.proxy_request( + request, + collections_filter=collections_filter, + stream=True, + ) return StreamingResponse( rp_resp.aiter_raw(), status_code=rp_resp.status_code, diff --git a/src/stac_auth_proxy/utils.py b/src/stac_auth_proxy/utils.py index 8f047c5..342a91e 100644 --- a/src/stac_auth_proxy/utils.py +++ b/src/stac_auth_proxy/utils.py @@ -3,7 +3,10 @@ import re from urllib.parse import urlparse +from cql2 import Expr +from fastapi import Request from fastapi.dependencies.models import Dependant +from starlette.datastructures import QueryParams from httpx import Headers @@ -42,3 +45,47 @@ def has_any_security_requirements(dependency: Dependant) -> bool: return any( has_any_security_requirements(sub_dep) for sub_dep in dependency.dependencies ) + + +async def apply_filter(request: Request, filter: Expr) -> Request: + """Apply a CQL2 filter to a request.""" + req_filter = request.query_params.get("filter") or ( + (await request.json()).get("filter") + if request.headers.get("content-length") + else None + ) + + new_filter = Expr(" AND ".join(e.to_text() for e in [req_filter, filter] if e)) + new_filter.validate() + + if request.method == "GET": + updated_scope = request.scope.copy() + updated_scope["query_string"] = update_qs( + request.query_params, + filter=new_filter.to_text(), + ) + return Request( + scope=updated_scope, + receive=request.receive, + # send=request._send, + ) + + # TODO: Support POST/PUT/PATCH + # elif request.method == "POST": + # request_body = await request.body() + # query = request.url.query + # query += "&" if query else "?" + # query += f"filter={filter}" + # request.url.query = query + + return request + + +def update_qs(query_params: QueryParams, **updates) -> bytes: + query_dict = { + **query_params, + **updates, + } + return "&".join(f"{key}={value}" for key, value in query_dict.items()).encode( + "utf-8" + ) From e5eee66e664af698849fdc7b0b29781bf562cf75 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 12 Dec 2024 20:20:04 -0800 Subject: [PATCH 09/29] Passing tests --- src/stac_auth_proxy/app.py | 8 +-- src/stac_auth_proxy/handlers/reverse_proxy.py | 46 ++++++------- src/stac_auth_proxy/utils.py | 69 +++++++------------ 3 files changed, 51 insertions(+), 72 deletions(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index c5df655..1574a7c 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -6,16 +6,16 @@ """ import logging -from typing import Optional, Annotated +from typing import Optional -from fastapi import FastAPI, Security, Request, Depends -from cql2 import Expr +from fastapi import FastAPI, Security from .auth import OpenIdConnectAuth from .config import Settings from .handlers import OpenApiSpecHandler, ReverseProxyHandler from .middleware import AddProcessTimeHeaderMiddleware -from .utils import apply_filter + +# from .utils import apply_filter logger = logging.getLogger(__name__) diff --git a/src/stac_auth_proxy/handlers/reverse_proxy.py b/src/stac_auth_proxy/handlers/reverse_proxy.py index 4416985..c743fde 100644 --- a/src/stac_auth_proxy/handlers/reverse_proxy.py +++ b/src/stac_auth_proxy/handlers/reverse_proxy.py @@ -3,16 +3,16 @@ import logging import time from dataclasses import dataclass -from typing import Optional, Annotated +from typing import Annotated, Optional -from cql2 import Expr import httpx -from fastapi import Request, Depends +from cql2 import Expr +from fastapi import Depends, Request from starlette.background import BackgroundTask from starlette.datastructures import MutableHeaders from starlette.responses import StreamingResponse -from ..utils import update_qs +from .. import utils logger = logging.getLogger(__name__) @@ -33,18 +33,18 @@ def __post_init__(self): timeout=httpx.Timeout(timeout=15.0), ) - self.proxy_request.__annotations__["collections_filter"] = Annotated[ - Optional[Expr], Depends(self.collections_filter.dependency) - ] - self.stream.__annotations__["collections_filter"] = Annotated[ - Optional[Expr], Depends(self.collections_filter.dependency) - ] + # Update annotations to support FastAPI's dependency injection + for endpoint in [self.proxy_request, self.stream]: + endpoint.__annotations__["collections_filter"] = Annotated[ + Optional[Expr], + Depends(getattr(self.collections_filter, "dependency", lambda: None)), + ] async def proxy_request( self, request: Request, *, - collections_filter: Annotated[Optional[Expr], Depends(...)], + collections_filter: Annotated[Optional[Expr], Depends(...)] = None, stream=False, ) -> httpx.Response: """Proxy a request to the upstream STAC API.""" @@ -53,24 +53,22 @@ async def proxy_request( headers.setdefault("X-Forwarded-Host", request.url.hostname) path = request.url.path - query = request.url.query.encode("utf-8") + query = request.url.query - # https://github.com/fastapi/fastapi/discussions/7382#discussioncomment-5136466 - # TODO: Examine filters - if collections_filter: + if utils.is_collection_endpoint(path) and collections_filter: if request.method == "GET" and path == "/collections": - query += b"&" + update_qs( - request.query_params, filter=collections_filter.to_text() - ) - - url = httpx.URL( - path=path, - query=query, - ) + query = utils.insert_filter(qs=query, filter=collections_filter) + elif utils.is_item_endpoint(path) and self.items_filter: + if request.method == "GET": + query = utils.insert_filter(qs=query, filter=self.items_filter) + # https://github.com/fastapi/fastapi/discussions/7382#discussioncomment-5136466 rp_req = self.client.build_request( request.method, - url=url, + url=httpx.URL( + path=path, + query=query.encode("utf-8"), + ), headers=headers, content=request.stream(), ) diff --git a/src/stac_auth_proxy/utils.py b/src/stac_auth_proxy/utils.py index 342a91e..f2116b5 100644 --- a/src/stac_auth_proxy/utils.py +++ b/src/stac_auth_proxy/utils.py @@ -1,12 +1,10 @@ """Utility functions.""" import re -from urllib.parse import urlparse +from urllib.parse import parse_qs, urlencode, urlparse from cql2 import Expr -from fastapi import Request from fastapi.dependencies.models import Dependant -from starlette.datastructures import QueryParams from httpx import Headers @@ -47,45 +45,28 @@ def has_any_security_requirements(dependency: Dependant) -> bool: ) -async def apply_filter(request: Request, filter: Expr) -> Request: - """Apply a CQL2 filter to a request.""" - req_filter = request.query_params.get("filter") or ( - (await request.json()).get("filter") - if request.headers.get("content-length") - else None - ) +def insert_filter(qs: str, filter: Expr) -> str: + """Insert a filter expression into a query string. If a filter already exists, combine them.""" + qs_dict = parse_qs(qs) - new_filter = Expr(" AND ".join(e.to_text() for e in [req_filter, filter] if e)) - new_filter.validate() - - if request.method == "GET": - updated_scope = request.scope.copy() - updated_scope["query_string"] = update_qs( - request.query_params, - filter=new_filter.to_text(), - ) - return Request( - scope=updated_scope, - receive=request.receive, - # send=request._send, - ) - - # TODO: Support POST/PUT/PATCH - # elif request.method == "POST": - # request_body = await request.body() - # query = request.url.query - # query += "&" if query else "?" - # query += f"filter={filter}" - # request.url.query = query - - return request - - -def update_qs(query_params: QueryParams, **updates) -> bytes: - query_dict = { - **query_params, - **updates, - } - return "&".join(f"{key}={value}" for key, value in query_dict.items()).encode( - "utf-8" - ) + filters = [Expr(f) for f in qs_dict.get("filter", [])] + filters.append(filter) + + combined_filter = Expr(" AND ".join(e.to_text() for e in filters)) + combined_filter.validate() + + qs_dict["filter"] = [combined_filter.to_text()] + + return urlencode(qs_dict, doseq=True) + + +def is_collection_endpoint(path: str) -> bool: + """Check if the path is a collection endpoint.""" + # TODO: Expand this to cover all cases where a collection filter should be applied + return path == "/collections" + + +def is_item_endpoint(path: str) -> bool: + """Check if the path is an item endpoint.""" + # TODO: Expand this to cover all cases where an item filter should be applied + return path == "/collection/{collection_id}/items" From 8ddb3f694c84423640f33515a4202661da899612 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 12 Dec 2024 20:21:06 -0800 Subject: [PATCH 10/29] Lint fixes --- src/stac_auth_proxy/filters/template.py | 2 ++ src/stac_auth_proxy/handlers/reverse_proxy.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/stac_auth_proxy/filters/template.py b/src/stac_auth_proxy/filters/template.py index 2499c2d..51e0fb5 100644 --- a/src/stac_auth_proxy/filters/template.py +++ b/src/stac_auth_proxy/filters/template.py @@ -27,6 +27,8 @@ def __post_init__(self): self.dependency = self.build() def build(self): + """Generate a dependency for rendering a CQL2 filter expression.""" + async def dependency( request: Request, auth_token=Security(self.token_dependency) ) -> Expr: diff --git a/src/stac_auth_proxy/handlers/reverse_proxy.py b/src/stac_auth_proxy/handlers/reverse_proxy.py index c743fde..c0d5943 100644 --- a/src/stac_auth_proxy/handlers/reverse_proxy.py +++ b/src/stac_auth_proxy/handlers/reverse_proxy.py @@ -3,7 +3,7 @@ import logging import time from dataclasses import dataclass -from typing import Annotated, Optional +from typing import Annotated, Callable, Optional import httpx from cql2 import Expr @@ -23,8 +23,8 @@ class ReverseProxyHandler: upstream: str client: httpx.AsyncClient = None - collections_filter: Optional[callable] = None - items_filter: Optional[callable] = None + collections_filter: Optional[Callable] = None + items_filter: Optional[Callable] = None def __post_init__(self): """Initialize the HTTP client.""" From d4757dae269dcd21cfc95b1c6737d089417a3727 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 12 Dec 2024 20:31:13 -0800 Subject: [PATCH 11/29] Cleanup --- src/stac_auth_proxy/app.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 1574a7c..0233fd6 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -60,20 +60,6 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: oidc_config_url=str(settings.oidc_discovery_url), ) - # @app.get("/collections") - # async def collections( - # request: Request, - # filter: Annotated[Optional[Expr], Depends(collections_filter.dependency)], - # ): - # # if filter: - # # print(f"{request.receive=}") - # # request = await apply_filter( - # # request, - # # filter, - # # ) - # # print(f"{request.receive=}") - # return await proxy_handler.stream(request=request) - # Endpoints that are explicitely marked private for path, methods in settings.private_endpoints.items(): app.add_api_route( From a34c37065c43a74039532a182e1aeb1636125421 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 12 Dec 2024 20:31:35 -0800 Subject: [PATCH 12/29] Mv from dataclasses to higher order functions --- src/stac_auth_proxy/filters/template.py | 77 ++++++++----------- src/stac_auth_proxy/handlers/reverse_proxy.py | 3 +- 2 files changed, 33 insertions(+), 47 deletions(-) diff --git a/src/stac_auth_proxy/filters/template.py b/src/stac_auth_proxy/filters/template.py index 51e0fb5..45fa2ef 100644 --- a/src/stac_auth_proxy/filters/template.py +++ b/src/stac_auth_proxy/filters/template.py @@ -1,7 +1,6 @@ """Generate CQL2 filter expressions via Jinja2 templating.""" -from dataclasses import dataclass, field -from typing import Any, Callable +from typing import Any, Annotated, Callable from cql2 import Expr from fastapi import Request, Security @@ -10,48 +9,34 @@ from ..utils import extract_variables -@dataclass -class Template: +def Template(template_str: str, token_dependency: Callable[..., Any]): """Generate CQL2 filter expressions via Jinja2 templating.""" - - template_str: str - token_dependency: Callable[..., Any] - - # Generated attributes - env: Environment = field(init=False) - dependency: Callable[[Request, Security], Expr] = field(init=False) - - def __post_init__(self): - """Initialize the Jinja2 environment.""" - self.env = Environment(loader=BaseLoader).from_string(self.template_str) - self.dependency = self.build() - - def build(self): - """Generate a dependency for rendering a CQL2 filter expression.""" - - async def dependency( - request: Request, auth_token=Security(self.token_dependency) - ) -> Expr: - """Render a CQL2 filter expression with the request and auth token.""" - # TODO: How to handle the case where auth_token is null? - context = { - "req": { - "path": request.url.path, - "method": request.method, - "query_params": dict(request.query_params), - "path_params": extract_variables(request.url.path), - "headers": dict(request.headers), - "body": ( - await request.json() - if request.headers.get("content-type") == "application/json" - else (await request.body()).decode() - ), - }, - "token": auth_token, - } - cql2_str = self.env.render(**context) - cql2_expr = Expr(cql2_str) - cql2_expr.validate() - return cql2_expr - - return dependency + env = Environment(loader=BaseLoader).from_string(template_str) + + async def dependency( + request: Request, + auth_token=Annotated[dict[str, Any], Security(token_dependency)], + ) -> Expr: + """Render a CQL2 filter expression with the request and auth token.""" + # TODO: How to handle the case where auth_token is null? + context = { + "req": { + "path": request.url.path, + "method": request.method, + "query_params": dict(request.query_params), + "path_params": extract_variables(request.url.path), + "headers": dict(request.headers), + "body": ( + await request.json() + if request.headers.get("content-type") == "application/json" + else (await request.body()).decode() + ), + }, + "token": auth_token, + } + cql2_str = env.render(**context) + cql2_expr = Expr(cql2_str) + cql2_expr.validate() + return cql2_expr + + return dependency diff --git a/src/stac_auth_proxy/handlers/reverse_proxy.py b/src/stac_auth_proxy/handlers/reverse_proxy.py index c0d5943..841b83f 100644 --- a/src/stac_auth_proxy/handlers/reverse_proxy.py +++ b/src/stac_auth_proxy/handlers/reverse_proxy.py @@ -37,7 +37,7 @@ def __post_init__(self): for endpoint in [self.proxy_request, self.stream]: endpoint.__annotations__["collections_filter"] = Annotated[ Optional[Expr], - Depends(getattr(self.collections_filter, "dependency", lambda: None)), + Depends(self.collections_filter or (lambda: None)), ] async def proxy_request( @@ -55,6 +55,7 @@ async def proxy_request( path = request.url.path query = request.url.query + # Appliy filters if utils.is_collection_endpoint(path) and collections_filter: if request.method == "GET" and path == "/collections": query = utils.insert_filter(qs=query, filter=collections_filter) From e62175058686eefcad1839763d48b1a8e5708847 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 12 Dec 2024 20:49:49 -0800 Subject: [PATCH 13/29] Legibility refactor (more higher order functions instead of dataclasses) --- src/stac_auth_proxy/app.py | 8 +- src/stac_auth_proxy/auth.py | 132 ++++++++++-------- src/stac_auth_proxy/filters/template.py | 2 +- src/stac_auth_proxy/handlers/__init__.py | 4 +- src/stac_auth_proxy/handlers/open_api_spec.py | 28 ++-- 5 files changed, 90 insertions(+), 84 deletions(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 0233fd6..1893e37 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -12,7 +12,7 @@ from .auth import OpenIdConnectAuth from .config import Settings -from .handlers import OpenApiSpecHandler, ReverseProxyHandler +from .handlers import ReverseProxyHandler, build_openapi_spec_handler from .middleware import AddProcessTimeHeaderMiddleware # from .utils import apply_filter @@ -55,7 +55,7 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: collections_filter=collections_filter, items_filter=items_filter, ) - openapi_handler = OpenApiSpecHandler( + openapi_handler = build_openapi_spec_handler( proxy=proxy_handler, oidc_config_url=str(settings.oidc_discovery_url), ) @@ -67,7 +67,7 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: ( proxy_handler.stream if path != settings.openapi_spec_endpoint - else openapi_handler.dispatch + else openapi_handler ), methods=methods, dependencies=[Security(auth_scheme.validated_user)], @@ -80,7 +80,7 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: ( proxy_handler.stream if path != settings.openapi_spec_endpoint - else openapi_handler.dispatch + else openapi_handler ), methods=methods, dependencies=[Security(auth_scheme.maybe_validated_user)], diff --git a/src/stac_auth_proxy/auth.py b/src/stac_auth_proxy/auth.py index 1e4826e..1c43dea 100644 --- a/src/stac_auth_proxy/auth.py +++ b/src/stac_auth_proxy/auth.py @@ -4,7 +4,7 @@ import logging import urllib.request from dataclasses import dataclass, field -from typing import Annotated, Any, Callable, Optional, Sequence +from typing import Annotated, Optional, Sequence import jwt from fastapi import HTTPException, Security, security, status @@ -25,8 +25,6 @@ class OpenIdConnectAuth: # Generated attributes auth_scheme: SecurityBase = field(init=False) jwks_client: jwt.PyJWKClient = field(init=False) - validated_user: Callable[..., Any] = field(init=False) - maybe_validated_user: Callable[..., Any] = field(init=False) def __post_init__(self): """Initialize the OIDC authentication class.""" @@ -50,70 +48,80 @@ def __post_init__(self): openIdConnectUrl=str(self.openid_configuration_url), auto_error=False, ) - self.validated_user = self._build(auto_error=True) - self.maybe_validated_user = self._build(auto_error=False) - - def _build(self, auto_error: bool = True): - """Build a dependency for validating an OIDC token.""" - - def valid_token_dependency( - auth_header: Annotated[str, Security(self.auth_scheme)], - required_scopes: security.SecurityScopes, - ): - """Dependency to validate an OIDC token.""" - if not auth_header: + + # Update annotations to support FastAPI's dependency injection + for endpoint in [self.validated_user, self.maybe_validated_user]: + endpoint.__annotations__["auth_header"] = Annotated[ + str, + Security(self.auth_scheme), + ] + + def maybe_validated_user( + self, + auth_header: Annotated[str, Security(...)], + required_scopes: security.SecurityScopes, + ): + """Dependency to validate an OIDC token.""" + return self.validated_user(auth_header, required_scopes, auto_error=False) + + def validated_user( + self, + auth_header: Annotated[str, Security(...)], + required_scopes: security.SecurityScopes, + auto_error: bool = True, + ): + """Dependency to validate an OIDC token.""" + if not auth_header: + if auto_error: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not authenticated", + ) + return None + + # Extract token from header + token_parts = auth_header.split(" ") + if len(token_parts) != 2 or token_parts[0].lower() != "bearer": + logger.error(f"Invalid token: {auth_header}") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + [_, token] = token_parts + + # Parse & validate token + try: + key = self.jwks_client.get_signing_key_from_jwt(token).key + payload = jwt.decode( + token, + key, + algorithms=["RS256"], + # NOTE: Audience validation MUST match audience claim if set in token (https://pyjwt.readthedocs.io/en/stable/changelog.html?highlight=audience#id40) + audience=self.allowed_jwt_audiences, + ) + except (jwt.exceptions.InvalidTokenError, jwt.exceptions.DecodeError) as e: + logger.exception(f"InvalidTokenError: {e=}") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) from e + + # Validate scopes (if required) + for scope in required_scopes.scopes: + if scope not in payload["scope"]: if auto_error: raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Not authenticated", + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={ + "WWW-Authenticate": f'Bearer scope="{required_scopes.scope_str}"' + }, ) return None - # Extract token from header - token_parts = auth_header.split(" ") - if len(token_parts) != 2 or token_parts[0].lower() != "bearer": - logger.error(f"Invalid token: {auth_header}") - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - [_, token] = token_parts - - # Parse & validate token - try: - key = self.jwks_client.get_signing_key_from_jwt(token).key - payload = jwt.decode( - token, - key, - algorithms=["RS256"], - # NOTE: Audience validation MUST match audience claim if set in token (https://pyjwt.readthedocs.io/en/stable/changelog.html?highlight=audience#id40) - audience=self.allowed_jwt_audiences, - ) - except (jwt.exceptions.InvalidTokenError, jwt.exceptions.DecodeError) as e: - logger.exception(f"InvalidTokenError: {e=}") - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) from e - - # Validate scopes (if required) - for scope in required_scopes.scopes: - if scope not in payload["scope"]: - if auto_error: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not enough permissions", - headers={ - "WWW-Authenticate": f'Bearer scope="{required_scopes.scope_str}"' - }, - ) - return None - - return payload - - return valid_token_dependency + return payload class OidcFetchError(Exception): diff --git a/src/stac_auth_proxy/filters/template.py b/src/stac_auth_proxy/filters/template.py index 45fa2ef..dd6de37 100644 --- a/src/stac_auth_proxy/filters/template.py +++ b/src/stac_auth_proxy/filters/template.py @@ -1,6 +1,6 @@ """Generate CQL2 filter expressions via Jinja2 templating.""" -from typing import Any, Annotated, Callable +from typing import Annotated, Any, Callable from cql2 import Expr from fastapi import Request, Security diff --git a/src/stac_auth_proxy/handlers/__init__.py b/src/stac_auth_proxy/handlers/__init__.py index 43d2dc3..7b03225 100644 --- a/src/stac_auth_proxy/handlers/__init__.py +++ b/src/stac_auth_proxy/handlers/__init__.py @@ -1,6 +1,6 @@ """Handlers to process requests.""" -from .open_api_spec import OpenApiSpecHandler +from .open_api_spec import build_openapi_spec_handler from .reverse_proxy import ReverseProxyHandler -__all__ = ["OpenApiSpecHandler", "ReverseProxyHandler"] +__all__ = ["build_openapi_spec_handler", "ReverseProxyHandler"] diff --git a/src/stac_auth_proxy/handlers/open_api_spec.py b/src/stac_auth_proxy/handlers/open_api_spec.py index 444f1d9..096f263 100644 --- a/src/stac_auth_proxy/handlers/open_api_spec.py +++ b/src/stac_auth_proxy/handlers/open_api_spec.py @@ -1,7 +1,6 @@ """Custom request handlers.""" import logging -from dataclasses import dataclass from fastapi import Request, Response from fastapi.routing import APIRoute @@ -12,17 +11,16 @@ logger = logging.getLogger(__name__) -@dataclass -class OpenApiSpecHandler: - """Handler for OpenAPI spec requests.""" +def build_openapi_spec_handler( + proxy: ReverseProxyHandler, + oidc_config_url: str, + auth_scheme_name: str = "oidcAuth", +): + """OpenAPI spec handler factory.""" - proxy: ReverseProxyHandler - oidc_config_url: str - auth_scheme_name: str = "oidcAuth" - - async def dispatch(self, req: Request, res: Response): + async def dispatch(req: Request, res: Response): """Proxy the OpenAPI spec from the upstream STAC API, updating it with OIDC security requirements.""" - oidc_spec_response = await self.proxy.proxy_request(req) + oidc_spec_response = await proxy.proxy_request(req) openapi_spec = oidc_spec_response.json() # Pass along the response headers @@ -45,10 +43,10 @@ async def dispatch(self, req: Request, res: Response): # Add the OIDC security scheme to the components openapi_spec.setdefault("components", {}).setdefault("securitySchemes", {})[ - self.auth_scheme_name + auth_scheme_name ] = { "type": "openIdConnect", - "openIdConnectUrl": self.oidc_config_url, + "openIdConnectUrl": oidc_config_url, } # Update the paths with the specified security requirements @@ -61,9 +59,9 @@ async def dispatch(self, req: Request, res: Response): if match.name != "FULL": continue # Add the OIDC security requirement - config.setdefault("security", []).append( - {self.auth_scheme_name: []} - ) + config.setdefault("security", []).append({auth_scheme_name: []}) break return openapi_spec + + return dispatch From 1069f412e07f88bb962f511e176e45262d9a6914 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 12 Dec 2024 20:56:19 -0800 Subject: [PATCH 14/29] Cleanup --- src/stac_auth_proxy/app.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 1893e37..00943e5 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -15,8 +15,6 @@ from .handlers import ReverseProxyHandler, build_openapi_spec_handler from .middleware import AddProcessTimeHeaderMiddleware -# from .utils import apply_filter - logger = logging.getLogger(__name__) @@ -40,20 +38,18 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: methods=["GET"], ) - collections_filter = ( - settings.collections_filter(auth_scheme.maybe_validated_user) - if settings.collections_filter - else None - ) - items_filter = ( - settings.items_filter(auth_scheme.maybe_validated_user) - if settings.items_filter - else None - ) proxy_handler = ReverseProxyHandler( upstream=str(settings.upstream_url), - collections_filter=collections_filter, - items_filter=items_filter, + collections_filter=( + settings.collections_filter(auth_scheme.maybe_validated_user) + if settings.collections_filter + else None + ), + items_filter=( + settings.items_filter(auth_scheme.maybe_validated_user) + if settings.items_filter + else None + ), ) openapi_handler = build_openapi_spec_handler( proxy=proxy_handler, From 4429fe4e9e927d8df8e1f357ae8108fa501310c9 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 12 Dec 2024 21:06:35 -0800 Subject: [PATCH 15/29] Add stub test --- tests/test_filters_jinja2.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/test_filters_jinja2.py diff --git a/tests/test_filters_jinja2.py b/tests/test_filters_jinja2.py new file mode 100644 index 0000000..ff8a2c4 --- /dev/null +++ b/tests/test_filters_jinja2.py @@ -0,0 +1,33 @@ +"""Tests for Jinja2 CQL2 filter.""" + +import pytest +from fastapi.testclient import TestClient +from utils import AppFactory + +app_factory = AppFactory( + oidc_discovery_url="https://example-stac-api.com/.well-known/openid-configuration", + default_public=False, +) + + +def test_collections_filter_contained_by_token(source_api_server, token_builder): + """""" + app = app_factory( + upstream_url=source_api_server, + collections_filter={ + "cls": "stac_auth_proxy.filters.Template", + "args": [ + "A_CONTAINEDBY(id, ( '{{ token.collections | join(\"', '\") }}' ))" + ], + }, + ) + client = TestClient( + app, + headers={ + "Authorization": f"Bearer {token_builder({"collections": ["foo", "bar"]})}" + }, + ) + response = client.get("/collections") + assert response.status_code == 200 + + # TODO: We need to verify that the upstream API was called with an applied filter From 3e4dd25befe6de07fc4c4e0247a2425fc032faa9 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Fri, 13 Dec 2024 12:19:11 -0800 Subject: [PATCH 16/29] fix: correct annotation --- src/stac_auth_proxy/filters/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stac_auth_proxy/filters/template.py b/src/stac_auth_proxy/filters/template.py index dd6de37..a9ba310 100644 --- a/src/stac_auth_proxy/filters/template.py +++ b/src/stac_auth_proxy/filters/template.py @@ -15,7 +15,7 @@ def Template(template_str: str, token_dependency: Callable[..., Any]): async def dependency( request: Request, - auth_token=Annotated[dict[str, Any], Security(token_dependency)], + auth_token: Annotated[dict[str, Any], Security(token_dependency)], ) -> Expr: """Render a CQL2 filter expression with the request and auth token.""" # TODO: How to handle the case where auth_token is null? From 7021043f729399b8d06f8115087131b28043c0c1 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Fri, 13 Dec 2024 12:19:26 -0800 Subject: [PATCH 17/29] update test server to avoid conflicts --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index bdf013e..7e3c744 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -120,7 +120,7 @@ def source_api(): @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 + host, port = "127.0.0.1", 9119 server = uvicorn.Server( uvicorn.Config( source_api, From 166ca414e6d5e3bf62333a5f248eef1f52d893c9 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Fri, 13 Dec 2024 12:22:17 -0800 Subject: [PATCH 18/29] Add functional test for CQL2 filter --- tests/test_filters_jinja2.py | 56 ++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/tests/test_filters_jinja2.py b/tests/test_filters_jinja2.py index ff8a2c4..c35afeb 100644 --- a/tests/test_filters_jinja2.py +++ b/tests/test_filters_jinja2.py @@ -1,5 +1,11 @@ """Tests for Jinja2 CQL2 filter.""" +from dataclasses import dataclass +from typing import Generator +from unittest.mock import AsyncMock, MagicMock, patch +from urllib.parse import parse_qs + +import httpx import pytest from fastapi.testclient import TestClient from utils import AppFactory @@ -10,24 +16,58 @@ ) -def test_collections_filter_contained_by_token(source_api_server, token_builder): - """""" +@pytest.fixture +def mock_send() -> Generator[MagicMock, None, None]: + """Mock the HTTPX send method. Useful when we want to inspect the request is sent to upstream API.""" + with patch( + "stac_auth_proxy.handlers.reverse_proxy.httpx.AsyncClient.send", + new_callable=AsyncMock, + ) as mock_send_method: + yield mock_send_method + + +@dataclass +class SingleChunkAsyncStream(httpx.AsyncByteStream): + """Mock async stream that returns a single chunk of data.""" + + body: bytes + + async def __aiter__(self): + """Return a single chunk of data.""" + yield self.body + + +def test_collections_filter_contained_by_token( + mock_send, source_api_server, token_builder +): + """Test that the collections filter is applied correctly.""" + # Mock response from upstream API + mock_send.return_value = httpx.Response( + 200, + stream=SingleChunkAsyncStream(b"{}"), + headers={"content-type": "application/json"}, + ) + app = app_factory( upstream_url=source_api_server, collections_filter={ "cls": "stac_auth_proxy.filters.Template", "args": [ - "A_CONTAINEDBY(id, ( '{{ token.collections | join(\"', '\") }}' ))" + "A_CONTAINEDBY(id, ('{{ token.collections | join(\"', '\") }}' ))" ], }, ) + + auth_token = token_builder({"collections": ["foo", "bar"]}) client = TestClient( app, - headers={ - "Authorization": f"Bearer {token_builder({"collections": ["foo", "bar"]})}" - }, + headers={"Authorization": f"Bearer {auth_token}"}, ) + response = client.get("/collections") assert response.status_code == 200 - - # TODO: We need to verify that the upstream API was called with an applied filter + assert mock_send.call_count == 1 + [r] = mock_send.call_args[0] + assert parse_qs(r.url.query.decode()) == { + "filter": ["a_containedby(id, ('foo', 'bar'))"] + } From 67f6b3af8dac0427032e41aa363dff003892bb4b Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Sat, 14 Dec 2024 19:59:38 -0800 Subject: [PATCH 19/29] Continue test buildout --- tests/conftest.py | 14 +++++- tests/test_filters_jinja2.py | 82 ++++++++++++++++++++---------------- tests/utils.py | 25 +++++++++++ 3 files changed, 82 insertions(+), 39 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7e3c744..5c74ae7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,8 +3,8 @@ import json import os import threading -from typing import Any -from unittest.mock import MagicMock, patch +from typing import Any, Generator +from unittest.mock import AsyncMock, MagicMock, patch import pytest import uvicorn @@ -140,3 +140,13 @@ def mock_env(): """Clear environment variables to avoid poluting configs from runtime env.""" with patch.dict(os.environ, clear=True): yield + + +@pytest.fixture +def mock_upstream() -> Generator[MagicMock, None, None]: + """Mock the HTTPX send method. Useful when we want to inspect the request is sent to upstream API.""" + with patch( + "stac_auth_proxy.handlers.reverse_proxy.httpx.AsyncClient.send", + new_callable=AsyncMock, + ) as mock_send_method: + yield mock_send_method diff --git a/tests/test_filters_jinja2.py b/tests/test_filters_jinja2.py index c35afeb..834d82f 100644 --- a/tests/test_filters_jinja2.py +++ b/tests/test_filters_jinja2.py @@ -1,8 +1,5 @@ """Tests for Jinja2 CQL2 filter.""" -from dataclasses import dataclass -from typing import Generator -from unittest.mock import AsyncMock, MagicMock, patch from urllib.parse import parse_qs import httpx @@ -10,43 +7,20 @@ from fastapi.testclient import TestClient from utils import AppFactory +from tests.utils import single_chunk_async_stream_response + app_factory = AppFactory( oidc_discovery_url="https://example-stac-api.com/.well-known/openid-configuration", default_public=False, ) -@pytest.fixture -def mock_send() -> Generator[MagicMock, None, None]: - """Mock the HTTPX send method. Useful when we want to inspect the request is sent to upstream API.""" - with patch( - "stac_auth_proxy.handlers.reverse_proxy.httpx.AsyncClient.send", - new_callable=AsyncMock, - ) as mock_send_method: - yield mock_send_method - - -@dataclass -class SingleChunkAsyncStream(httpx.AsyncByteStream): - """Mock async stream that returns a single chunk of data.""" - - body: bytes - - async def __aiter__(self): - """Return a single chunk of data.""" - yield self.body - - def test_collections_filter_contained_by_token( - mock_send, source_api_server, token_builder + mock_upstream, source_api_server, token_builder ): """Test that the collections filter is applied correctly.""" # Mock response from upstream API - mock_send.return_value = httpx.Response( - 200, - stream=SingleChunkAsyncStream(b"{}"), - headers={"content-type": "application/json"}, - ) + mock_upstream.return_value = single_chunk_async_stream_response(b"{}") app = app_factory( upstream_url=source_api_server, @@ -59,15 +33,49 @@ def test_collections_filter_contained_by_token( ) auth_token = token_builder({"collections": ["foo", "bar"]}) - client = TestClient( - app, - headers={"Authorization": f"Bearer {auth_token}"}, - ) - + client = TestClient(app, headers={"Authorization": f"Bearer {auth_token}"}) response = client.get("/collections") + assert response.status_code == 200 - assert mock_send.call_count == 1 - [r] = mock_send.call_args[0] + assert mock_upstream.call_count == 1 + [r] = mock_upstream.call_args[0] assert parse_qs(r.url.query.decode()) == { "filter": ["a_containedby(id, ('foo', 'bar'))"] } + + +@pytest.mark.parametrize( + "authenticated, expected_filter", + [ + (True, "true"), + (False, "(private = false)"), + ], +) +def test_collections_filter_private_and_public( + mock_upstream, source_api_server, token_builder, authenticated, expected_filter +): + """Test that filter can be used for private/public collections.""" + # Mock response from upstream API + mock_upstream.return_value = single_chunk_async_stream_response(b"{}") + + app = app_factory( + upstream_url=source_api_server, + collections_filter={ + "cls": "stac_auth_proxy.filters.Template", + "args": ["{{ '(private = false)' if token is none else true }}"], + }, + default_public=True, + ) + + client = TestClient( + app, + headers=( + {"Authorization": f"Bearer {token_builder({})}"} if authenticated else {} + ), + ) + response = client.get("/collections") + + assert response.status_code == 200 + assert mock_upstream.call_count == 1 + [r] = mock_upstream.call_args[0] + assert parse_qs(r.url.query.decode()) == {"filter": [expected_filter]} diff --git a/tests/utils.py b/tests/utils.py index f6374b0..8635931 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,10 @@ """Utilities for testing.""" +from dataclasses import dataclass from typing import Callable +import httpx + from stac_auth_proxy import Settings, create_app @@ -23,3 +26,25 @@ def __call__(self, *, upstream_url, **overrides) -> Callable: }, ) ) + + +@dataclass +class SingleChunkAsyncStream(httpx.AsyncByteStream): + """Mock async stream that returns a single chunk of data.""" + + body: bytes + + async def __aiter__(self): + """Return a single chunk of data.""" + yield self.body + + +def single_chunk_async_stream_response( + body: bytes, status_code=200, headers={"content-type": "application/json"} +): + """Create a response with a single chunk of data.""" + return httpx.Response( + stream=SingleChunkAsyncStream(body), + status_code=status_code, + headers=headers, + ) From 2ea5ab93f612d91a3e6a276bcd26c853ba9f5ad4 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 2 Jan 2025 13:13:08 -0800 Subject: [PATCH 20/29] Reorg utils --- src/stac_auth_proxy/app.py | 16 +++++ src/stac_auth_proxy/config.py | 6 +- src/stac_auth_proxy/filters/template.py | 2 +- src/stac_auth_proxy/handlers/open_api_spec.py | 3 +- src/stac_auth_proxy/handlers/reverse_proxy.py | 14 ++-- src/stac_auth_proxy/utils.py | 72 ------------------- src/stac_auth_proxy/utils/__init__.py | 0 src/stac_auth_proxy/utils/di.py | 13 ++++ src/stac_auth_proxy/utils/filters.py | 32 +++++++++ src/stac_auth_proxy/utils/requests.py | 29 ++++++++ 10 files changed, 106 insertions(+), 81 deletions(-) delete mode 100644 src/stac_auth_proxy/utils.py create mode 100644 src/stac_auth_proxy/utils/__init__.py create mode 100644 src/stac_auth_proxy/utils/di.py create mode 100644 src/stac_auth_proxy/utils/filters.py create mode 100644 src/stac_auth_proxy/utils/requests.py diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 00943e5..9b3079d 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -56,6 +56,22 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: oidc_config_url=str(settings.oidc_discovery_url), ) + # TODO: How can we inject the collections_filter into only the endpoints that need it? + # for endpoint, methods in settings.collections_filter_endpoints.items(): + # app.add_api_route( + # endpoint, + # partial( + # proxy_handler.stream, collections_filter=settings.collections_filter + # ), + # methods=methods, + # dependencies=[Security(auth_scheme.maybe_validated_user)], + # ) + + # skip = [ + # settings.openapi_spec_endpoint, + # *settings.collections_filter_endpoints, + # ] + # Endpoints that are explicitely marked private for path, methods in settings.private_endpoints.items(): app.add_api_route( diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index 19f77d2..9607ad2 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -18,7 +18,7 @@ class ClassInput(BaseModel): kwargs: dict[str, str] = Field(default_factory=dict) def __call__(self, token_dependency): - """Dynamically load a class and instantiate it with kwargs.""" + """Dynamically load a class and instantiate it with args & kwargs.""" module_path, class_name = self.cls.rsplit(".", 1) module = importlib.import_module(module_path) cls = getattr(module, class_name) @@ -49,6 +49,10 @@ class Settings(BaseSettings): openapi_spec_endpoint: Optional[str] = None collections_filter: Optional[ClassInput] = None + collections_filter_endpoints: Optional[EndpointMethods] = { + "/collections": ["GET"], + "/collections/{collection_id}": ["GET"], + } items_filter: Optional[ClassInput] = None model_config = SettingsConfigDict(env_prefix="STAC_AUTH_PROXY_") diff --git a/src/stac_auth_proxy/filters/template.py b/src/stac_auth_proxy/filters/template.py index a9ba310..1892c8b 100644 --- a/src/stac_auth_proxy/filters/template.py +++ b/src/stac_auth_proxy/filters/template.py @@ -6,7 +6,7 @@ from fastapi import Request, Security from jinja2 import BaseLoader, Environment -from ..utils import extract_variables +from ..utils.requests import extract_variables def Template(template_str: str, token_dependency: Callable[..., Any]): diff --git a/src/stac_auth_proxy/handlers/open_api_spec.py b/src/stac_auth_proxy/handlers/open_api_spec.py index 096f263..77a7fad 100644 --- a/src/stac_auth_proxy/handlers/open_api_spec.py +++ b/src/stac_auth_proxy/handlers/open_api_spec.py @@ -5,7 +5,8 @@ from fastapi import Request, Response from fastapi.routing import APIRoute -from ..utils import has_any_security_requirements, safe_headers +from ..utils.di import has_any_security_requirements +from ..utils.requests import safe_headers from .reverse_proxy import ReverseProxyHandler logger = logging.getLogger(__name__) diff --git a/src/stac_auth_proxy/handlers/reverse_proxy.py b/src/stac_auth_proxy/handlers/reverse_proxy.py index 841b83f..ed5660b 100644 --- a/src/stac_auth_proxy/handlers/reverse_proxy.py +++ b/src/stac_auth_proxy/handlers/reverse_proxy.py @@ -12,7 +12,7 @@ from starlette.datastructures import MutableHeaders from starlette.responses import StreamingResponse -from .. import utils +from ..utils import filters logger = logging.getLogger(__name__) @@ -23,6 +23,8 @@ class ReverseProxyHandler: upstream: str client: httpx.AsyncClient = None + + # Filters collections_filter: Optional[Callable] = None items_filter: Optional[Callable] = None @@ -55,13 +57,13 @@ async def proxy_request( path = request.url.path query = request.url.query - # Appliy filters - if utils.is_collection_endpoint(path) and collections_filter: + # Apply filters + if filters.is_collection_endpoint(path) and collections_filter: if request.method == "GET" and path == "/collections": - query = utils.insert_filter(qs=query, filter=collections_filter) - elif utils.is_item_endpoint(path) and self.items_filter: + query = filters.insert_filter(qs=query, filter=collections_filter) + elif filters.is_item_endpoint(path) and self.items_filter: if request.method == "GET": - query = utils.insert_filter(qs=query, filter=self.items_filter) + query = filters.insert_filter(qs=query, filter=self.items_filter) # https://github.com/fastapi/fastapi/discussions/7382#discussioncomment-5136466 rp_req = self.client.build_request( diff --git a/src/stac_auth_proxy/utils.py b/src/stac_auth_proxy/utils.py deleted file mode 100644 index f2116b5..0000000 --- a/src/stac_auth_proxy/utils.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Utility functions.""" - -import re -from urllib.parse import parse_qs, urlencode, urlparse - -from cql2 import Expr -from fastapi.dependencies.models import Dependant -from httpx import Headers - - -def safe_headers(headers: Headers) -> dict[str, str]: - """Scrub headers that should not be proxied to the client.""" - excluded_headers = [ - "content-length", - "content-encoding", - ] - return { - key: value - for key, value in headers.items() - if key.lower() not in excluded_headers - } - - -def extract_variables(url: str) -> dict: - """ - Extract variables from a URL path. Being that we use a catch-all endpoint for the proxy, - we can't rely on the path parameters that FastAPI provides. - """ - path = urlparse(url).path - # This allows either /items or /bulk_items, with an optional item_id following. - pattern = r"^/collections/(?P[^/]+)(?:/(?:items|bulk_items)(?:/(?P[^/]+))?)?/?$" - match = re.match(pattern, path) - return {k: v for k, v in match.groupdict().items() if v} if match else {} - - -def has_any_security_requirements(dependency: Dependant) -> bool: - """ - Recursively check if any dependency within the hierarchy has a non-empty - security_requirements list. - """ - if dependency.security_requirements: - return True - return any( - has_any_security_requirements(sub_dep) for sub_dep in dependency.dependencies - ) - - -def insert_filter(qs: str, filter: Expr) -> str: - """Insert a filter expression into a query string. If a filter already exists, combine them.""" - qs_dict = parse_qs(qs) - - filters = [Expr(f) for f in qs_dict.get("filter", [])] - filters.append(filter) - - combined_filter = Expr(" AND ".join(e.to_text() for e in filters)) - combined_filter.validate() - - qs_dict["filter"] = [combined_filter.to_text()] - - return urlencode(qs_dict, doseq=True) - - -def is_collection_endpoint(path: str) -> bool: - """Check if the path is a collection endpoint.""" - # TODO: Expand this to cover all cases where a collection filter should be applied - return path == "/collections" - - -def is_item_endpoint(path: str) -> bool: - """Check if the path is an item endpoint.""" - # TODO: Expand this to cover all cases where an item filter should be applied - return path == "/collection/{collection_id}/items" diff --git a/src/stac_auth_proxy/utils/__init__.py b/src/stac_auth_proxy/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/stac_auth_proxy/utils/di.py b/src/stac_auth_proxy/utils/di.py new file mode 100644 index 0000000..759375d --- /dev/null +++ b/src/stac_auth_proxy/utils/di.py @@ -0,0 +1,13 @@ +from fastapi.dependencies.models import Dependant + + +def has_any_security_requirements(dependency: Dependant) -> bool: + """ + Recursively check if any dependency within the hierarchy has a non-empty + security_requirements list. + """ + if dependency.security_requirements: + return True + return any( + has_any_security_requirements(sub_dep) for sub_dep in dependency.dependencies + ) diff --git a/src/stac_auth_proxy/utils/filters.py b/src/stac_auth_proxy/utils/filters.py new file mode 100644 index 0000000..cc21af6 --- /dev/null +++ b/src/stac_auth_proxy/utils/filters.py @@ -0,0 +1,32 @@ +"""Utility functions.""" + +from urllib.parse import parse_qs, urlencode + +from cql2 import Expr + + +def insert_filter(qs: str, filter: Expr) -> str: + """Insert a filter expression into a query string. If a filter already exists, combine them.""" + qs_dict = parse_qs(qs) + + filters = [Expr(f) for f in qs_dict.get("filter", [])] + filters.append(filter) + + combined_filter = Expr(" AND ".join(e.to_text() for e in filters)) + combined_filter.validate() + + qs_dict["filter"] = [combined_filter.to_text()] + + return urlencode(qs_dict, doseq=True) + + +def is_collection_endpoint(path: str) -> bool: + """Check if the path is a collection endpoint.""" + # TODO: Expand this to cover all cases where a collection filter should be applied + return path == "/collections" + + +def is_item_endpoint(path: str) -> bool: + """Check if the path is an item endpoint.""" + # TODO: Expand this to cover all cases where an item filter should be applied + return path == "/collection/{collection_id}/items" diff --git a/src/stac_auth_proxy/utils/requests.py b/src/stac_auth_proxy/utils/requests.py new file mode 100644 index 0000000..431d093 --- /dev/null +++ b/src/stac_auth_proxy/utils/requests.py @@ -0,0 +1,29 @@ +import re + +from urllib.parse import urlparse +from httpx import Headers + + +def safe_headers(headers: Headers) -> dict[str, str]: + """Scrub headers that should not be proxied to the client.""" + excluded_headers = [ + "content-length", + "content-encoding", + ] + return { + key: value + for key, value in headers.items() + if key.lower() not in excluded_headers + } + + +def extract_variables(url: str) -> dict: + """ + Extract variables from a URL path. Being that we use a catch-all endpoint for the proxy, + we can't rely on the path parameters that FastAPI provides. + """ + path = urlparse(url).path + # This allows either /items or /bulk_items, with an optional item_id following. + pattern = r"^/collections/(?P[^/]+)(?:/(?:items|bulk_items)(?:/(?P[^/]+))?)?/?$" + match = re.match(pattern, path) + return {k: v for k, v in match.groupdict().items() if v} if match else {} From 00b6a8a642848e1e1ccec92800791cadbc07a208 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 2 Jan 2025 15:35:55 -0800 Subject: [PATCH 21/29] Initial pass at DI tooling --- pyproject.toml | 1 + src/stac_auth_proxy/app.py | 30 +--------- src/stac_auth_proxy/config.py | 4 +- src/stac_auth_proxy/filters/template.py | 8 +-- src/stac_auth_proxy/handlers/reverse_proxy.py | 51 ++++++++++------- src/stac_auth_proxy/utils/__init__.py | 1 + src/stac_auth_proxy/utils/di.py | 43 +++++++++++++- src/stac_auth_proxy/utils/requests.py | 4 +- tests/test_di.py | 56 +++++++++++++++++++ tests/test_filters_jinja2.py | 1 - tests/test_utils.py | 2 +- uv.lock | 14 +++++ 12 files changed, 155 insertions(+), 60 deletions(-) create mode 100644 tests/test_di.py diff --git a/pyproject.toml b/pyproject.toml index 5de22e1..314eb05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ requires = ["hatchling>=1.12.0"] dev = [ "jwcrypto>=1.5.6", "pre-commit>=3.5.0", + "pytest-asyncio>=0.25.1", "pytest-cov>=5.0.0", "pytest>=8.3.3", ] diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 9b3079d..8540881 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -40,38 +40,14 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: proxy_handler = ReverseProxyHandler( upstream=str(settings.upstream_url), - collections_filter=( - settings.collections_filter(auth_scheme.maybe_validated_user) - if settings.collections_filter - else None - ), - items_filter=( - settings.items_filter(auth_scheme.maybe_validated_user) - if settings.items_filter - else None - ), + auth_dependency=auth_scheme.maybe_validated_user, + collections_filter=settings.collections_filter, + items_filter=settings.items_filter, ) openapi_handler = build_openapi_spec_handler( proxy=proxy_handler, oidc_config_url=str(settings.oidc_discovery_url), ) - - # TODO: How can we inject the collections_filter into only the endpoints that need it? - # for endpoint, methods in settings.collections_filter_endpoints.items(): - # app.add_api_route( - # endpoint, - # partial( - # proxy_handler.stream, collections_filter=settings.collections_filter - # ), - # methods=methods, - # dependencies=[Security(auth_scheme.maybe_validated_user)], - # ) - - # skip = [ - # settings.openapi_spec_endpoint, - # *settings.collections_filter_endpoints, - # ] - # Endpoints that are explicitely marked private for path, methods in settings.private_endpoints.items(): app.add_api_route( diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index 9607ad2..af49420 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -17,12 +17,12 @@ class ClassInput(BaseModel): args: Sequence[str] = Field(default_factory=list) kwargs: dict[str, str] = Field(default_factory=dict) - def __call__(self, token_dependency): + def __call__(self): """Dynamically load a class and instantiate it with args & kwargs.""" module_path, class_name = self.cls.rsplit(".", 1) module = importlib.import_module(module_path) cls = getattr(module, class_name) - return cls(*self.args, **self.kwargs, token_dependency=token_dependency) + return cls(*self.args, **self.kwargs) class Settings(BaseSettings): diff --git a/src/stac_auth_proxy/filters/template.py b/src/stac_auth_proxy/filters/template.py index 1892c8b..fecc380 100644 --- a/src/stac_auth_proxy/filters/template.py +++ b/src/stac_auth_proxy/filters/template.py @@ -1,21 +1,21 @@ """Generate CQL2 filter expressions via Jinja2 templating.""" -from typing import Annotated, Any, Callable +from typing import Annotated, Any from cql2 import Expr -from fastapi import Request, Security +from fastapi import Request from jinja2 import BaseLoader, Environment from ..utils.requests import extract_variables -def Template(template_str: str, token_dependency: Callable[..., Any]): +def Template(template_str: str): """Generate CQL2 filter expressions via Jinja2 templating.""" env = Environment(loader=BaseLoader).from_string(template_str) async def dependency( request: Request, - auth_token: Annotated[dict[str, Any], Security(token_dependency)], + auth_token: Annotated[dict[str, Any], ...], ) -> Expr: """Render a CQL2 filter expression with the request and auth token.""" # TODO: How to handle the case where auth_token is null? diff --git a/src/stac_auth_proxy/handlers/reverse_proxy.py b/src/stac_auth_proxy/handlers/reverse_proxy.py index ed5660b..d8a1cc9 100644 --- a/src/stac_auth_proxy/handlers/reverse_proxy.py +++ b/src/stac_auth_proxy/handlers/reverse_proxy.py @@ -12,7 +12,7 @@ from starlette.datastructures import MutableHeaders from starlette.responses import StreamingResponse -from ..utils import filters +from ..utils import di, filters logger = logging.getLogger(__name__) @@ -22,6 +22,8 @@ class ReverseProxyHandler: """Reverse proxy functionality.""" upstream: str + auth_dependency: Callable + client: httpx.AsyncClient = None # Filters @@ -34,21 +36,21 @@ def __post_init__(self): base_url=self.upstream, timeout=httpx.Timeout(timeout=15.0), ) + self.collections_filter = ( + self.collections_filter() if self.collections_filter else None + ) + self.items_filter = self.items_filter() if self.items_filter else None - # Update annotations to support FastAPI's dependency injection - for endpoint in [self.proxy_request, self.stream]: - endpoint.__annotations__["collections_filter"] = Annotated[ + # Inject auth dependency into filters + for endpoint in [self.collections_filter, self.items_filter]: + if not endpoint: + continue + endpoint.__annotations__["auth_token"] = Annotated[ Optional[Expr], - Depends(self.collections_filter or (lambda: None)), + Depends(self.auth_dependency), ] - async def proxy_request( - self, - request: Request, - *, - collections_filter: Annotated[Optional[Expr], Depends(...)] = None, - stream=False, - ) -> httpx.Response: + async def proxy_request(self, request: Request, *, stream=False) -> httpx.Response: """Proxy a request to the upstream STAC API.""" headers = MutableHeaders(request.headers) headers.setdefault("X-Forwarded-For", request.client.host) @@ -58,12 +60,23 @@ async def proxy_request( query = request.url.query # Apply filters - if filters.is_collection_endpoint(path) and collections_filter: - if request.method == "GET" and path == "/collections": + if filters.is_collection_endpoint(path) and self.collections_filter: + collections_filter = await di.call_with_injected_dependencies( + func=self.collections_filter, + request=request, + ) + if request.method == "GET": query = filters.insert_filter(qs=query, filter=collections_filter) - elif filters.is_item_endpoint(path) and self.items_filter: + else: + # TODO: Augment body + ... + + if filters.is_item_endpoint(path) and self.items_filter: if request.method == "GET": query = filters.insert_filter(qs=query, filter=self.items_filter) + else: + # TODO: Augment body + ... # https://github.com/fastapi/fastapi/discussions/7382#discussioncomment-5136466 rp_req = self.client.build_request( @@ -87,15 +100,11 @@ async def proxy_request( rp_resp.headers["X-Upstream-Time"] = f"{proxy_time:.3f}" return rp_resp - async def stream( - self, - request: Request, - collections_filter: Annotated[Optional[Expr], Depends(...)], - ) -> StreamingResponse: + async def stream(self, request: Request) -> StreamingResponse: """Transparently proxy a request to the upstream STAC API.""" rp_resp = await self.proxy_request( request, - collections_filter=collections_filter, + # collections_filter=collections_filter, stream=True, ) return StreamingResponse( diff --git a/src/stac_auth_proxy/utils/__init__.py b/src/stac_auth_proxy/utils/__init__.py index e69de29..489dfcf 100644 --- a/src/stac_auth_proxy/utils/__init__.py +++ b/src/stac_auth_proxy/utils/__init__.py @@ -0,0 +1 @@ +"""Utils module for stac_auth_proxy.""" diff --git a/src/stac_auth_proxy/utils/di.py b/src/stac_auth_proxy/utils/di.py index 759375d..48643de 100644 --- a/src/stac_auth_proxy/utils/di.py +++ b/src/stac_auth_proxy/utils/di.py @@ -1,4 +1,14 @@ +"""Dependency injection utilities for FastAPI.""" + +import asyncio + +from fastapi import Request from fastapi.dependencies.models import Dependant +from fastapi.dependencies.utils import ( + get_parameterless_sub_dependant, + solve_dependencies, +) +from fastapi.params import Depends def has_any_security_requirements(dependency: Dependant) -> bool: @@ -6,8 +16,35 @@ def has_any_security_requirements(dependency: Dependant) -> bool: Recursively check if any dependency within the hierarchy has a non-empty security_requirements list. """ - if dependency.security_requirements: - return True - return any( + return dependency.security_requirements or any( has_any_security_requirements(sub_dep) for sub_dep in dependency.dependencies ) + + +async def call_with_injected_dependencies(func, request: Request): + """ + Manually solves and injects dependencies for `func` using FastAPI's internal + dependency injection machinery. + """ + dependant = get_parameterless_sub_dependant( + depends=Depends(dependency=func), + path=request.url.path, + ) + + solved = await solve_dependencies( + request=request, + dependant=dependant, + # response=response, + # body=request.body, + body=None, + async_exit_stack=None, + embed_body_fields=False, + ) + + if solved.errors: + raise RuntimeError(f"Dependency resolution error: {solved.errors}") + + results = func(**solved.values) + if asyncio.iscoroutine(results): + return await results + return results diff --git a/src/stac_auth_proxy/utils/requests.py b/src/stac_auth_proxy/utils/requests.py index 431d093..a422cf6 100644 --- a/src/stac_auth_proxy/utils/requests.py +++ b/src/stac_auth_proxy/utils/requests.py @@ -1,6 +1,8 @@ -import re +"""Utility functions for working with HTTP requests.""" +import re from urllib.parse import urlparse + from httpx import Headers diff --git a/tests/test_di.py b/tests/test_di.py new file mode 100644 index 0000000..c2e5071 --- /dev/null +++ b/tests/test_di.py @@ -0,0 +1,56 @@ +"""Tests for the dependency injection utility.""" + +from typing import Annotated + +import pytest +from fastapi import Depends, Request + +from stac_auth_proxy.utils.di import call_with_injected_dependencies + + +async def get_db_connection(): + """Mock asynchronous function to get a DB connection.""" + # pretend you open a DB connection or retrieve a session + return "some_db_connection" + + +def get_special_value(): + """Mock synchronous function to get a special value.""" + return 42 + + +async def async_func_with_dependencies( + db_conn: Annotated[str, Depends(get_db_connection)], + special_value: Annotated[int, Depends(get_special_value)], +): + """Mock asynchronous dependency.""" + return (db_conn, special_value) + + +def sync_func_with_dependencies( + db_conn: Annotated[str, Depends(get_db_connection)], + special_value: Annotated[int, Depends(get_special_value)], +): + """Mock synchronous dependency.""" + return (db_conn, special_value) + + +@pytest.mark.parametrize( + "func", + [async_func_with_dependencies, sync_func_with_dependencies], +) +@pytest.mark.asyncio +async def test_di(func): + """Test dependency injection.""" + request = Request( + scope={ + "type": "http", + "method": "GET", + "path": "/test", + "headers": [], + "query_string": b"", + } + ) + + result = await call_with_injected_dependencies(func, request=request) + assert result == ("some_db_connection", 42) diff --git a/tests/test_filters_jinja2.py b/tests/test_filters_jinja2.py index 834d82f..a2b1314 100644 --- a/tests/test_filters_jinja2.py +++ b/tests/test_filters_jinja2.py @@ -2,7 +2,6 @@ from urllib.parse import parse_qs -import httpx import pytest from fastapi.testclient import TestClient from utils import AppFactory diff --git a/tests/test_utils.py b/tests/test_utils.py index 01faeae..bfb6cab 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,7 +2,7 @@ import pytest -from stac_auth_proxy.utils import extract_variables +from stac_auth_proxy.utils.requests import extract_variables @pytest.mark.parametrize( diff --git a/uv.lock b/uv.lock index 17f076e..313e1f2 100644 --- a/uv.lock +++ b/uv.lock @@ -849,6 +849,18 @@ 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 = "pytest-asyncio" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/04/0477a4bdd176ad678d148c075f43620b3f7a060ff61c7da48500b1fa8a75/pytest_asyncio-0.25.1.tar.gz", hash = "sha256:79be8a72384b0c917677e00daa711e07db15259f4d23203c59012bcd989d4aee", size = 53760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/fb/efc7226b384befd98d0e00d8c4390ad57f33c8fde00094b85c5e07897def/pytest_asyncio-0.25.1-py3-none-any.whl", hash = "sha256:c84878849ec63ff2ca509423616e071ef9cd8cc93c053aa33b5b8fb70a990671", size = 19357 }, +] + [[package]] name = "pytest-cov" version = "5.0.0" @@ -954,6 +966,7 @@ dev = [ { name = "jwcrypto" }, { name = "pre-commit" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-cov" }, ] @@ -975,6 +988,7 @@ dev = [ { name = "jwcrypto", specifier = ">=1.5.6" }, { name = "pre-commit", specifier = ">=3.5.0" }, { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-asyncio", specifier = ">=0.25.1" }, { name = "pytest-cov", specifier = ">=5.0.0" }, ] From 08e4f405769573d962ea6a3e52c21dddd0c3e41c Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 2 Jan 2025 18:36:03 -0800 Subject: [PATCH 22/29] Fix import --- tests/test_filters_jinja2.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_filters_jinja2.py b/tests/test_filters_jinja2.py index a2b1314..a9b5d72 100644 --- a/tests/test_filters_jinja2.py +++ b/tests/test_filters_jinja2.py @@ -4,9 +4,7 @@ import pytest from fastapi.testclient import TestClient -from utils import AppFactory - -from tests.utils import single_chunk_async_stream_response +from utils import AppFactory, single_chunk_async_stream_response app_factory = AppFactory( oidc_discovery_url="https://example-stac-api.com/.well-known/openid-configuration", From acbfa631143709e853a24a3aa56f89db0c5d236e Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 2 Jan 2025 23:30:19 -0800 Subject: [PATCH 23/29] Add test config --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 314eb05..1f4c888 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,3 +47,7 @@ dev = [ "pytest-cov>=5.0.0", "pytest>=8.3.3", ] + +[tool.pytest.ini_options] +asyncio_default_fixture_loop_scope = "function" +asyncio_mode = "strict" From f0c3c6a32cc2efa612eba8e50ca967a0cd35126a Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 2 Jan 2025 23:30:57 -0800 Subject: [PATCH 24/29] Reorg tests --- tests/conftest.py | 3 +++ tests/test_filters_jinja2.py | 8 +------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5c74ae7..ce83376 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ import uvicorn from fastapi import FastAPI from jwcrypto import jwk, jwt +from utils import single_chunk_async_stream_response @pytest.fixture @@ -149,4 +150,6 @@ def mock_upstream() -> Generator[MagicMock, None, None]: "stac_auth_proxy.handlers.reverse_proxy.httpx.AsyncClient.send", new_callable=AsyncMock, ) as mock_send_method: + # Mock response from upstream API + mock_send_method.return_value = single_chunk_async_stream_response(b"{}") yield mock_send_method diff --git a/tests/test_filters_jinja2.py b/tests/test_filters_jinja2.py index a9b5d72..b4d8bc2 100644 --- a/tests/test_filters_jinja2.py +++ b/tests/test_filters_jinja2.py @@ -4,7 +4,7 @@ import pytest from fastapi.testclient import TestClient -from utils import AppFactory, single_chunk_async_stream_response +from utils import AppFactory app_factory = AppFactory( oidc_discovery_url="https://example-stac-api.com/.well-known/openid-configuration", @@ -16,9 +16,6 @@ def test_collections_filter_contained_by_token( mock_upstream, source_api_server, token_builder ): """Test that the collections filter is applied correctly.""" - # Mock response from upstream API - mock_upstream.return_value = single_chunk_async_stream_response(b"{}") - app = app_factory( upstream_url=source_api_server, collections_filter={ @@ -52,9 +49,6 @@ def test_collections_filter_private_and_public( mock_upstream, source_api_server, token_builder, authenticated, expected_filter ): """Test that filter can be used for private/public collections.""" - # Mock response from upstream API - mock_upstream.return_value = single_chunk_async_stream_response(b"{}") - app = app_factory( upstream_url=source_api_server, collections_filter={ From 680b32ebd4f0d1af43b03f186622299edd331eb5 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 2 Jan 2025 23:31:12 -0800 Subject: [PATCH 25/29] combine filter logic --- src/stac_auth_proxy/handlers/reverse_proxy.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/stac_auth_proxy/handlers/reverse_proxy.py b/src/stac_auth_proxy/handlers/reverse_proxy.py index d8a1cc9..951d032 100644 --- a/src/stac_auth_proxy/handlers/reverse_proxy.py +++ b/src/stac_auth_proxy/handlers/reverse_proxy.py @@ -60,20 +60,19 @@ async def proxy_request(self, request: Request, *, stream=False) -> httpx.Respon query = request.url.query # Apply filters - if filters.is_collection_endpoint(path) and self.collections_filter: - collections_filter = await di.call_with_injected_dependencies( - func=self.collections_filter, + for is_match, filter_generator in [ + (filters.is_collection_endpoint(path), self.collections_filter), + (filters.is_item_endpoint(path), self.items_filter), + ]: + if not is_match or not filter_generator: + continue + + cql_filter = await di.call_with_injected_dependencies( + func=filter_generator, request=request, ) if request.method == "GET": - query = filters.insert_filter(qs=query, filter=collections_filter) - else: - # TODO: Augment body - ... - - if filters.is_item_endpoint(path) and self.items_filter: - if request.method == "GET": - query = filters.insert_filter(qs=query, filter=self.items_filter) + query = filters.insert_filter(qs=query, filter=cql_filter) else: # TODO: Augment body ... From 71e5ae1565af765c58720db52c137fdf0ade8545 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 2 Jan 2025 23:31:24 -0800 Subject: [PATCH 26/29] update item endpoint check --- src/stac_auth_proxy/utils/filters.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stac_auth_proxy/utils/filters.py b/src/stac_auth_proxy/utils/filters.py index cc21af6..d297004 100644 --- a/src/stac_auth_proxy/utils/filters.py +++ b/src/stac_auth_proxy/utils/filters.py @@ -1,5 +1,6 @@ """Utility functions.""" +import re from urllib.parse import parse_qs, urlencode from cql2 import Expr @@ -29,4 +30,4 @@ def is_collection_endpoint(path: str) -> bool: def is_item_endpoint(path: str) -> bool: """Check if the path is an item endpoint.""" # TODO: Expand this to cover all cases where an item filter should be applied - return path == "/collection/{collection_id}/items" + return bool(re.compile(r"^/collections/([^/]+)/items$").match(path)) From 39dd77bfc543f670636ed963ee742a00e3c49863 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 2 Jan 2025 23:31:28 -0800 Subject: [PATCH 27/29] test item filter --- tests/test_filters_jinja2.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_filters_jinja2.py b/tests/test_filters_jinja2.py index b4d8bc2..811f605 100644 --- a/tests/test_filters_jinja2.py +++ b/tests/test_filters_jinja2.py @@ -70,3 +70,37 @@ def test_collections_filter_private_and_public( assert mock_upstream.call_count == 1 [r] = mock_upstream.call_args[0] assert parse_qs(r.url.query.decode()) == {"filter": [expected_filter]} + + +@pytest.mark.parametrize( + "authenticated, expected_filter", + [ + (True, "true"), + (False, '("properties.private" = false)'), + ], +) +def test_items_filter_private_and_public( + mock_upstream, source_api_server, token_builder, authenticated, expected_filter +): + """Test that filter can be used for private/public collections.""" + app = app_factory( + upstream_url=source_api_server, + items_filter={ + "cls": "stac_auth_proxy.filters.Template", + "args": ["{{ '(properties.private = false)' if token is none else true }}"], + }, + default_public=True, + ) + + client = TestClient( + app, + headers=( + {"Authorization": f"Bearer {token_builder({})}"} if authenticated else {} + ), + ) + response = client.get("/collections/foo/items") + + assert response.status_code == 200 + assert mock_upstream.call_count == 1 + [r] = mock_upstream.call_args[0] + assert parse_qs(r.url.query.decode()) == {"filter": [expected_filter]} From b89cfce12927d613a8fe6b6b382f6ed4be45c24a Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 2 Jan 2025 23:34:59 -0800 Subject: [PATCH 28/29] Rm maybe auth check for public endpoints --- src/stac_auth_proxy/app.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 8540881..f03f5b2 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -71,7 +71,7 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: else openapi_handler ), methods=methods, - dependencies=[Security(auth_scheme.maybe_validated_user)], + dependencies=[], ) # Catchall for remainder of the endpoints @@ -80,13 +80,7 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: proxy_handler.stream, methods=["GET", "POST", "PUT", "PATCH", "DELETE"], dependencies=( - [ - Security( - auth_scheme.maybe_validated_user - if settings.default_public - else auth_scheme.validated_user - ) - ] + [] if settings.default_public else [Security(auth_scheme.validated_user)] ), ) From 76e5cb8e04e81b1da739f65158e6a0c943c33c3d Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Thu, 2 Jan 2025 23:44:32 -0800 Subject: [PATCH 29/29] Refactor for legibility --- src/stac_auth_proxy/app.py | 51 ++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index f03f5b2..d002677 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -23,14 +23,10 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: settings = settings or Settings() app = FastAPI( - openapi_url=None, + openapi_url=None, # Disable OpenAPI schema endpoint, we want to serve upstream's schema ) app.add_middleware(AddProcessTimeHeaderMiddleware) - auth_scheme = OpenIdConnectAuth( - openid_configuration_url=settings.oidc_discovery_url - ) - if settings.debug: app.add_api_route( "/_debug", @@ -38,6 +34,10 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: methods=["GET"], ) + # Tooling + auth_scheme = OpenIdConnectAuth( + openid_configuration_url=settings.oidc_discovery_url + ) proxy_handler = ReverseProxyHandler( upstream=str(settings.upstream_url), auth_dependency=auth_scheme.maybe_validated_user, @@ -48,31 +48,24 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI: proxy=proxy_handler, oidc_config_url=str(settings.oidc_discovery_url), ) - # Endpoints that are explicitely marked private - for path, methods in settings.private_endpoints.items(): - app.add_api_route( - path, - ( - proxy_handler.stream - if path != settings.openapi_spec_endpoint - else openapi_handler - ), - methods=methods, - dependencies=[Security(auth_scheme.validated_user)], - ) - # Endpoints that are explicitely marked as public - for path, methods in settings.public_endpoints.items(): - app.add_api_route( - path, - ( - proxy_handler.stream - if path != settings.openapi_spec_endpoint - else openapi_handler - ), - methods=methods, - dependencies=[], - ) + # Configure security dependency for explicitely specified endpoints + for path_methods, dependencies in [ + (settings.private_endpoints, [Security(auth_scheme.validated_user)]), + (settings.public_endpoints, []), + ]: + for path, methods in path_methods.items(): + endpoint = ( + openapi_handler + if path == settings.openapi_spec_endpoint + else proxy_handler.stream + ) + app.add_api_route( + path, + endpoint=endpoint, + methods=methods, + dependencies=dependencies, + ) # Catchall for remainder of the endpoints app.add_api_route(