From 51158585c21528115ea7ca18a067e2874d5c524f Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Sat, 2 Aug 2025 21:17:58 -0700 Subject: [PATCH] docs: build out separate documentation website --- .github/workflows/deploy-mkdocs.yaml | 33 + README.md | 480 ------------- docs/architecture/data-filtering.md | 195 ++++++ docs/architecture/middleware-stack.md | 39 ++ docs/assets/ds-symbol-negative-mono.png | Bin 0 -> 36420 bytes docs/assets/ds-symbol-positive-mono.png | Bin 0 -> 36816 bytes docs/configuration.md | 243 +++++++ docs/index.md | 1 + docs/installation-and-running.md | 58 ++ .../integrations/analytics/plausible.html | 53 ++ docs/tips.md | 28 + mkdocs.yml | 123 ++++ pyproject.toml | 11 + src/stac_auth_proxy/config.py | 8 +- uv.lock | 628 +++++++++++++++++- 15 files changed, 1415 insertions(+), 485 deletions(-) create mode 100644 .github/workflows/deploy-mkdocs.yaml create mode 100644 docs/architecture/data-filtering.md create mode 100644 docs/architecture/middleware-stack.md create mode 100644 docs/assets/ds-symbol-negative-mono.png create mode 100644 docs/assets/ds-symbol-positive-mono.png create mode 100644 docs/configuration.md create mode 120000 docs/index.md create mode 100644 docs/installation-and-running.md create mode 100644 docs/overrides/partials/integrations/analytics/plausible.html create mode 100644 docs/tips.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/deploy-mkdocs.yaml b/.github/workflows/deploy-mkdocs.yaml new file mode 100644 index 00000000..19aa8a6c --- /dev/null +++ b/.github/workflows/deploy-mkdocs.yaml @@ -0,0 +1,33 @@ +name: Publish docs via GitHub Pages + +on: + push: + branches: + - main + paths: + # Only rebuild website when docs have changed + - "README.md" + - "deployment/**" + - "docs/**" + - "src/**" + - ".github/workflows/deploy_mkdocs.yml" + +jobs: + build: + name: Deploy docs + runs-on: ubuntu-latest + steps: + - name: Checkout main + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + + - uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Deploy docs + run: uv run mkdocs gh-deploy --force diff --git a/README.md b/README.md index a51ae662..8a776e60 100644 --- a/README.md +++ b/README.md @@ -21,483 +21,3 @@ STAC Auth Proxy is a proxy API that mediates between the client and your interna - **🧩 Authentication Extension:** Add the [Authentication Extension](https://github.com/stac-extensions/authentication) to API responses to expose auth-related metadata - **📘 OpenAPI Augmentation:** Enhance the [OpenAPI spec](https://swagger.io/specification/) with security details to keep auto-generated docs and UIs (e.g., [Swagger UI](https://swagger.io/tools/swagger-ui/)) accurate - **🗜️ Response Compression:** Optimize response sizes using [`starlette-cramjam`](https://github.com/developmentseed/starlette-cramjam/) - -## Usage - -### Running - -#### Docker - -The simplest way to run the project is via Docker: - -```sh -docker run \ - -it --rm \ - -p 8000:8000 \ - -e UPSTREAM_URL=https://my-stac-api \ - -e OIDC_DISCOVERY_URL=https://my-auth-server/.well-known/openid-configuration \ - ghcr.io/developmentseed/stac-auth-proxy:latest -``` - -#### Python - -The installed Python module can be invoked directly: - -```sh -python -m stac_auth_proxy -``` - -#### Uvicorn - -The application's factory can be passed to Uvicorn: - -```sh -uvicorn --factory stac_auth_proxy:create_app -``` - -#### Docker Compose - -The codebase ships with a `docker-compose.yaml` file, allowing the proxy to be run locally alongside various supporting services: the database, the STAC API, and a Mock OIDC provider. - -##### pgSTAC Backend - -Run the application stack with a pgSTAC backend using [stac-fastapi-pgstac](https://github.com/stac-utils/stac-fastapi-pgstac): - -```sh -docker compose up -``` - -##### OpenSearch Backend - -Run the application stack with an OpenSearch backend using [stac-fastapi-elasticsearch-opensearch](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch): - -```sh -docker compose --profile os up -``` - -### Installation - -The application can be installed as a standard Python module: - -```sh -pip install stac-auth-proxy -``` - -For local development, we use [`uv`](https://docs.astral.sh/uv/) to manage project dependencies and environment. - -```sh -uv sync -``` - -### Configuration - -The application is configurable via environment variables. - -#### Core - -- **`UPSTREAM_URL`**, STAC API URL - - **Type:** HTTP(S) URL - - **Required:** Yes - - **Example:** `https://your-stac-api.com/stac` -- **`WAIT_FOR_UPSTREAM`**, wait for upstream API to become available before starting proxy - - **Type:** boolean - - **Required:** No, defaults to `true` - - **Example:** `false`, `1`, `True` -- **`CHECK_CONFORMANCE`**, ensure upstream API conforms to required conformance classes before starting proxy - - **Type:** boolean - - **Required:** No, defaults to `true` - - **Example:** `false`, `1`, `True` -- **`ENABLE_COMPRESSION`**, enable response compression - - **Type:** boolean - - **Required:** No, defaults to `true` - - **Example:** `false`, `1`, `True` -- **`HEALTHZ_PREFIX`**, path prefix for health check endpoints - - **Type:** string - - **Required:** No, defaults to `/healthz` - - **Example:** `''` (disabled) -- **`OVERRIDE_HOST`**, override the host header for the upstream API - - **Type:** boolean - - **Required:** No, defaults to `true` - - **Example:** `false`, `1`, `True` -- **`ROOT_PATH`**, path prefix for the proxy API - - **Type:** string - - **Required:** No, defaults to `''` (root path) - - **Example:** `/api/v1` - - **Note:** This is independent of the upstream API's path. The proxy will handle removing this prefix from incoming requests and adding it to outgoing links. - -#### Authentication - -- **`OIDC_DISCOVERY_URL`**, OpenID Connect discovery document URL - - **Type:** HTTP(S) URL - - **Required:** Yes - - **Example:** `https://auth.example.com/.well-known/openid-configuration` -- **`OIDC_DISCOVERY_INTERNAL_URL`**, internal network OpenID Connect discovery document URL - - **Type:** HTTP(S) URL - - **Required:** No, defaults to the value of `OIDC_DISCOVERY_URL` - - **Example:** `http://auth/.well-known/openid-configuration` -- **`DEFAULT_PUBLIC`**, default access policy for endpoints - - **Type:** boolean - - **Required:** No, defaults to `false` - - **Example:** `false`, `1`, `True` -- **`PRIVATE_ENDPOINTS`**, endpoints explicitly marked as requiring authentication and possibly scopes - - **Type:** JSON object mapping regex patterns to HTTP methods OR tuples of an HTTP method and string representing required scopes - - **Required:** No, defaults to the following: - ```json - { - "^/collections$": ["POST"], - "^/collections/([^/]+)$": ["PUT", "PATCH", "DELETE"], - "^/collections/([^/]+)/items$": ["POST"], - "^/collections/([^/]+)/items/([^/]+)$": ["PUT", "PATCH", "DELETE"], - "^/collections/([^/]+)/bulk_items$": ["POST"] - } - ``` -- **`PUBLIC_ENDPOINTS`**, endpoints explicitly marked as not requiring authentication, used when `DEFAULT_PUBLIC == False` - - **Type:** JSON object mapping regex patterns to HTTP methods - - **Required:** No, defaults to the following: - ```json - { - "^/api.html$": ["GET"], - "^/api$": ["GET"], - "^/docs/oauth2-redirect": ["GET"], - "^/healthz": ["GET"] - } - ``` -- **`ENABLE_AUTHENTICATION_EXTENSION`**, enable authentication extension in STAC API responses - - **Type:** boolean - - **Required:** No, defaults to `true` - - **Example:** `false`, `1`, `True` - -#### OpenAPI / Swagger UI - -- **`OPENAPI_SPEC_ENDPOINT`**, path of OpenAPI specification, used for augmenting spec response with auth configuration - - **Type:** string or null - - **Required:** No, defaults to `null` (disabled) - - **Example:** `/api` -- **`OPENAPI_AUTH_SCHEME_NAME`**, name of the auth scheme to use in the OpenAPI spec - - **Type:** string - - **Required:** No, defaults to `oidcAuth` - - **Example:** `jwtAuth` -- **`OPENAPI_AUTH_SCHEME_OVERRIDE`**, override for the auth scheme in the OpenAPI spec - - **Type:** JSON object - - **Required:** No, defaults to `null` (disabled) - - **Example:** `{"type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "Paste your raw JWT here. This API uses Bearer token authorization.\n"}` -- **`SWAGGER_UI_ENDPOINT`**, path of Swagger UI, used to indicate that a custom Swagger UI should be hosted, typically useful when providing accompanying `SWAGGER_UI_INIT_OAUTH` arguments - - **Type:** string or null - - **Required:** No, defaults to `null` (disabled) - - **Example:** `/api.html` -- **`SWAGGER_UI_INIT_OAUTH`**, initialization options for the [Swagger UI OAuth2 configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/) on custom Swagger UI - - **Type:** JSON object - - **Required:** No, defaults to `null` (disabled) - - **Example:** `{"clientId": "stac-auth-proxy", "usePkceWithAuthorizationCodeGrant": true}` - -#### Filtering - -- **`ITEMS_FILTER_CLS`**, CQL2 expression generator for item-level filtering - - **Type:** JSON object with class configuration - - **Required:** No, defaults to `null` (disabled) - - **Example:** `stac_auth_proxy.filters:Opa`, `stac_auth_proxy.filters:Template`, `my_package:OrganizationFilter` -- **`ITEMS_FILTER_ARGS`**, Positional arguments for CQL2 expression generator - - **Type:** List of positional arguments used to initialize the class - - **Required:** No, defaults to `[]` - - **Example:**: `["org1"]` -- **`ITEMS_FILTER_KWARGS`**, Keyword arguments for CQL2 expression generator - - **Type:** Dictionary of keyword arguments used to initialize the class - - **Required:** No, defaults to `{}` - - **Example:** `{"field_name": "properties.organization"}` -- **`ITEMS_FILTER_PATH`**, Regex pattern used to identify request paths that require the application of the items filter - - **Type:** Regex string - - **Required:** No, defaults to `^(/collections/([^/]+)/items(/[^/]+)?$|/search$)` - - **Example:** `^(/collections/([^/]+)/items(/[^/]+)?$|/search$|/custom$)` -- **`COLLECTIONS_FILTER_CLS`**, CQL2 expression generator for collection-level filtering - - **Type:** JSON object with class configuration - - **Required:** No, defaults to `null` (disabled) - - **Example:** `stac_auth_proxy.filters:Opa`, `stac_auth_proxy.filters:Template`, `my_package:OrganizationFilter` -- **`COLLECTIONS_FILTER_ARGS`**, Positional arguments for CQL2 expression generator - - **Type:** List of positional arguments used to initialize the class - - **Required:** No, defaults to `[]` - - **Example:**: `["org1"]` -- **`COLLECTIONS_FILTER_KWARGS`**, Keyword arguments for CQL2 expression generator - - **Type:** Dictionary of keyword arguments used to initialize the class - - **Required:** No, defaults to `{}` - - **Example:** `{"field_name": "properties.organization"}` -- **`COLLECTIONS_FILTER_PATH`**, Regex pattern used to identify request paths that require the application of the collections filter - - **Type:** Regex string - - **Required:** No, defaults to `^/collections(/[^/]+)?$` - - **Example:** `^.*?/collections(/[^/]+)?$` - -### Tips - -#### Root Paths - -The proxy can be optionally served from a non-root path (e.g., `/api/v1`). Additionally, the proxy can optionally proxy requests to an upstream API served from a non-root path (e.g., `/stac`). To handle this, the proxy will: - -- Remove the `ROOT_PATH` from incoming requests before forwarding to the upstream API -- Remove the proxy's prefix from all links in STAC API responses -- Add the `ROOT_PATH` prefix to all links in STAC API responses -- Update the OpenAPI specification to include the `ROOT_PATH` in the servers field -- Handle requests that don't match the `ROOT_PATH` with a 404 response - -#### Non-OIDC Workaround - -If the upstream server utilizes RS256 JWTs but does not utilize a proper OIDC server, the proxy can be configured to work around this by setting the `OIDC_DISCOVERY_URL` to a statically-hosted OIDC discovery document that points to a valid JWKS endpoint. Additionally, the OpenAPI can be configured to support direct JWT input, via: - -```sh -OPENAPI_AUTH_SCHEME_NAME=jwtAuth -OPENAPI_AUTH_SCHEME_OVERRIDE={"type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "Paste your raw JWT here. This API uses Bearer token authorization."} -``` - -### Customization - -While the project is designed to work out-of-the-box as an application, it might not address every projects needs. When the need for customization arises, the codebase can instead be treated as a library of components that can be used to augment any [ASGI](https://asgi.readthedocs.io/en/latest/)-compliant webserver (e.g. [Django](https://docs.djangoproject.com/en/3.0/topics/async/), [Falcon](https://falconframework.org/), [FastAPI](https://github.com/tiangolo/fastapi), [Litestar](https://litestar.dev/), [Responder](https://responder.readthedocs.io/en/latest/), [Sanic](https://sanic.dev/), [Starlette](https://www.starlette.io/)). Review [`app.py`](https://github.com/developmentseed/stac-auth-proxy/blob/main/src/stac_auth_proxy/app.py) to get a sense of how we make use of the various components to construct a FastAPI application. - -## Architecture - -### Middleware Stack - -The majority of the proxy's functionality occurs within a chain of middlewares. Each request passes through this chain, wherein each middleware performs a specific task: - -1. **`EnforceAuthMiddleware`** - - - Handles authentication and authorization - - Configurable public/private endpoints - - OIDC integration - - Places auth token payload in request state - -2. **`Cql2BuildFilterMiddleware`** - - - Builds CQL2 filters based on request context/state - - Places [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) in request state - -3. **`Cql2ApplyFilterQueryStringMiddleware`** - - - Retrieves [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) from request state - - Augments `GET` requests with CQL2 filter by appending to querystring - -4. **`Cql2ApplyFilterBodyMiddleware`** - - - Retrieves [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) from request state - - Augments `` POST`/`PUT`/`PATCH `` requests with CQL2 filter by modifying body - -5. **`Cql2ValidateResponseBodyMiddleware`** - - - Retrieves [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) from request state - - Validates response against CQL2 filter for non-filterable endpoints - -6. **`OpenApiMiddleware`** - - - Modifies OpenAPI specification based on endpoint configuration, adding security requirements - - Only active if `openapi_spec_endpoint` is configured - -7. **`AddProcessTimeHeaderMiddleware`** - - Adds processing time headers - - Useful for monitoring/debugging - -### Data filtering via CQL2 - -The system supports generating CQL2 filters based on request context to provide row-level content filtering. These CQL2 filters are then set on outgoing requests prior to the upstream API. - -> [!IMPORTANT] -> The upstream STAC API must support the [STAC API Filter Extension](https://github.com/stac-api-extensions/filter/blob/main/README.md), including the [Features Filter](http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter) conformance class on to the Features resource (`/collections/{cid}/items`)[^37]. - -#### Filters - -If enabled, filters are applied to the following endpoints: - -- `GET /search` - - **Supported:** ✅ - - **Action:** Read Item - - **Applied Filter:** `ITEMS_FILTER` - - **Strategy:** Append query params with generated CQL2 query. -- `POST /search` - - **Supported:** ✅ - - **Action:** Read Item - - **Applied Filter:** `ITEMS_FILTER` - - **Strategy:** Append body with generated CQL2 query. -- `GET /collections/{collection_id}/items` - - **Supported:** ✅ - - **Action:** Read Item - - **Applied Filter:** `ITEMS_FILTER` - - **Strategy:** Append query params with generated CQL2 query. -- `GET /collections/{collection_id}/items/{item_id}` - - **Supported:** ✅ - - **Action:** Read Item - - **Applied Filter:** `ITEMS_FILTER` - - **Strategy:** Validate response against CQL2 query. -- `GET /collections` - - **Supported:** ✅ - - **Action:** Read Collection - - **Applied Filter:** `COLLECTIONS_FILTER` - - **Strategy:** Append query params with generated CQL2 query. -- `GET /collections/{collection_id}` - - **Supported:** ✅ - - **Action:** Read Collection - - **Applied Filter:** `COLLECTIONS_FILTER` - - **Strategy:** Validate response against CQL2 query. -- `POST /collections/` - - **Supported:** ❌[^22] - - **Action:** Create Collection - - **Applied Filter:** `COLLECTIONS_FILTER` - - **Strategy:** Validate body with generated CQL2 query. -- `PUT /collections/{collection_id}}` - - **Supported:** ❌[^22] - - **Action:** Update Collection - - **Applied Filter:** `COLLECTIONS_FILTER` - - **Strategy:** Fetch Collection and validate CQL2 query; merge Item with body and validate with generated CQL2 query. -- `DELETE /collections/{collection_id}` - - **Supported:** ❌[^22] - - **Action:** Delete Collection - - **Applied Filter:** `COLLECTIONS_FILTER` - - **Strategy:** Fetch Collectiion and validate with CQL2 query. -- `POST /collections/{collection_id}/items` - - **Supported:** ❌[^21] - - **Action:** Create Item - - **Applied Filter:** `ITEMS_FILTER` - - **Strategy:** Validate body with generated CQL2 query. -- `PUT /collections/{collection_id}/items/{item_id}` - - **Supported:** ❌[^21] - - **Action:** Update Item - - **Applied Filter:** `ITEMS_FILTER` - - **Strategy:** Fetch Item and validate CQL2 query; merge Item with body and validate with generated CQL2 query. -- `DELETE /collections/{collection_id}/items/{item_id}` - - **Supported:** ❌[^21] - - **Action:** Delete Item - - **Applied Filter:** `ITEMS_FILTER` - - **Strategy:** Fetch Item and validate with CQL2 query. -- `POST /collections/{collection_id}/bulk_items` - - **Supported:** ❌[^21] - - **Action:** Create Items - - **Applied Filter:** `ITEMS_FILTER` - - **Strategy:** Validate items in body with generated CQL2 query. - -#### Example Request Flow for multi-record endpoints - -```mermaid -sequenceDiagram - Client->>Proxy: GET /collections - Note over Proxy: EnforceAuth checks credentials - Note over Proxy: BuildCql2Filter creates filter - Note over Proxy: ApplyCql2Filter applies filter to request - Proxy->>STAC API: GET /collection?filter=(collection=landsat) - STAC API->>Client: Response -``` - -#### Example Request Flow for single-record endpoints - -The Filter Extension does not apply to fetching individual records. As such, we must validate the record _after_ it is returned from the upstream API but _before_ it is returned to the user: - -```mermaid -sequenceDiagram - Client->>Proxy: GET /collections/abc123 - Note over Proxy: EnforceAuth checks credentials - Note over Proxy: BuildCql2Filter creates filter - Proxy->>STAC API: GET /collection/abc123 - Note over Proxy: ApplyCql2Filter validates the response - STAC API->>Client: Response -``` - -#### Authoring Filter Generators - -The `ITEMS_FILTER_CLS` configuration option can be used to specify a class that will be used to generate a CQL2 filter for the request. The class must define a `__call__` method that accepts a single argument: a dictionary containing the request context; and returns a valid `cql2-text` expression (as a `str`) or `cql2-json` expression (as a `dict`). - -> [!TIP] -> An example integration can be found in [`examples/custom-integration`](https://github.com/developmentseed/stac-auth-proxy/blob/main/examples/custom-integration). - -##### Basic Filter Generator - -```py -import dataclasses -from typing import Any - -from cql2 import Expr - - -@dataclasses.dataclass -class ExampleFilter: - async def __call__(self, context: dict[str, Any]) -> str: - return "true" -``` - -> [!TIP] -> Despite being referred to as a _class_, a filter generator could be written as a function. -> ->
-> -> Example -> -> ```py -> from typing import Any -> -> from cql2 import Expr -> -> -> def example_filter(): -> async def example_filter(context: dict[str, Any]) -> str | dict[str, Any]: -> return Expr("true") -> return example_filter -> ``` -> ->
- -##### Complex Filter Generator - -An example of a more complex filter generator where the filter is generated based on the response of an external API: - -```py -import dataclasses -from typing import Any - -from httpx import AsyncClient -from stac_auth_proxy.utils.cache import MemoryCache - - -@dataclasses.dataclass -class ApprovedCollectionsFilter: - api_url: str - kind: Literal["item", "collection"] = "item" - client: AsyncClient = dataclasses.field(init=False) - cache: MemoryCache = dataclasses.field(init=False) - - def __post_init__(self): - # We keep the client in the class instance to avoid creating a new client for - # each request, taking advantage of the client's connection pooling. - self.client = AsyncClient(base_url=self.api_url) - self.cache = MemoryCache(ttl=30) - - async def __call__(self, context: dict[str, Any]) -> dict[str, Any]: - token = context["req"]["headers"].get("authorization") - - try: - # Check cache for a previously generated filter - approved_collections = self.cache[token] - except KeyError: - # Lookup approved collections from an external API - approved_collections = await self.lookup(token) - self.cache[token] = approved_collections - - # Build CQL2 filter - return { - "op": "a_containedby", - "args": [ - {"property": "collection" if self.kind == "item" else "id"}, - approved_collections - ], - } - - async def lookup(self, token: Optional[str]) -> list[str]: - # Lookup approved collections from an external API - headers = {"Authorization": f"Bearer {token}"} if token else {} - response = await self.client.get( - f"/get-approved-collections", - headers=headers, - ) - response.raise_for_status() - return response.json()["collections"] -``` - -> [!TIP] -> Filter generation runs for every relevant request. Consider memoizing external API calls to improve performance. - -[^21]: https://github.com/developmentseed/stac-auth-proxy/issues/21 -[^22]: https://github.com/developmentseed/stac-auth-proxy/issues/22 -[^30]: https://github.com/developmentseed/stac-auth-proxy/issues/30 -[^37]: https://github.com/developmentseed/stac-auth-proxy/issues/37 diff --git a/docs/architecture/data-filtering.md b/docs/architecture/data-filtering.md new file mode 100644 index 00000000..0649113c --- /dev/null +++ b/docs/architecture/data-filtering.md @@ -0,0 +1,195 @@ +# Data filtering via CQL2 + +The system supports generating CQL2 filters based on request context to provide row-level content filtering. These CQL2 filters are then set on outgoing requests prior to the upstream API. + +> [!IMPORTANT] +> The upstream STAC API must support the [STAC API Filter Extension](https://github.com/stac-api-extensions/filter/blob/main/README.md), including the [Features Filter](http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter) conformance class on to the Features resource (`/collections/{cid}/items`)[^37]. + +## Filters + +### `ITEMS_FILTER` + +The [`ITEMS_FILTER`](../configuration.md#collections_filter_cls) is applied to the following operations. + +> [!WARNING] +> Operations without a check mark are not yet supported. We intend to support these operations within the future. + +- [x] `GET /search` + - **Action:** Read Item + - **Strategy:** Append query params with generated CQL2 query. +- [x] `POST /search` + - **Action:** Read Item + - **Strategy:** Append body with generated CQL2 query. +- [x] `GET /collections/{collection_id}/items` + - **Action:** Read Item + - **Strategy:** Append query params with generated CQL2 query. +- [x] `GET /collections/{collection_id}/items/{item_id}` + - **Action:** Read Item + - **Strategy:** Validate response against CQL2 query. +- [ ] `POST /collections/{collection_id}/items`[^21] + - **Action:** Create Item + - **Strategy:** Validate body with generated CQL2 query. +- [ ] `PUT /collections/{collection_id}/items/{item_id}`[^21] + - **Action:** Update Item + - **Strategy:** Fetch Item and validate CQL2 query; merge Item with body and validate with generated CQL2 query. +- [ ] `DELETE /collections/{collection_id}/items/{item_id}`[^21] + - **Action:** Delete Item + - **Strategy:** Fetch Item and validate with CQL2 query. +- [ ] `POST /collections/{collection_id}/bulk_items`[^21] + - **Action:** Create Items + - **Strategy:** Validate items in body with generated CQL2 query. + +### `COLLECTIONS_FILTER` + +The [`COLLECTIONS_FILTER`](../configuration#collections_filter_cls) applies to the following operations. + +> [!WARNING] +> Operations without a check mark are not yet supported. We intend to support these operations within the future. + +- [x] `GET /collections` + - **Action:** Read Collection + - **Strategy:** Append query params with generated CQL2 query. +- [x] `GET /collections/{collection_id}` + - **Action:** Read Collection + - **Strategy:** Validate response against CQL2 query. +- [ ] `POST /collections/`[^22] + - **Action:** Create Collection + - **Strategy:** Validate body with generated CQL2 query. +- [ ] `PUT /collections/{collection_id}`[^22] + - **Action:** Update Collection + - **Strategy:** Fetch Collection and validate CQL2 query; merge Item with body and validate with generated CQL2 query. +- [ ] `DELETE /collections/{collection_id}`[^22] + - **Action:** Delete Collection + - **Strategy:** Fetch Collection and validate with CQL2 query. + +## Example Request Flow for multi-record endpoints + +```mermaid +sequenceDiagram + Client->>Proxy: GET /collections + Note over Proxy: EnforceAuth checks credentials + Note over Proxy: BuildCql2Filter creates filter + Note over Proxy: ApplyCql2Filter applies filter to request + Proxy->>STAC API: GET /collection?filter=(collection=landsat) + STAC API->>Client: Response +``` + +## Example Request Flow for single-record endpoints + +The Filter Extension does not apply to fetching individual records. As such, we must validate the record _after_ it is returned from the upstream API but _before_ it is returned to the user: + +```mermaid +sequenceDiagram + Client->>Proxy: GET /collections/abc123 + Note over Proxy: EnforceAuth checks credentials + Note over Proxy: BuildCql2Filter creates filter + Proxy->>STAC API: GET /collection/abc123 + Note over Proxy: ApplyCql2Filter validates the response + STAC API->>Client: Response +``` + +## Authoring Filter Generators + +The `ITEMS_FILTER_CLS` configuration option can be used to specify a class that will be used to generate a CQL2 filter for the request. The class must define a `__call__` method that accepts a single argument: a dictionary containing the request context; and returns a valid `cql2-text` expression (as a `str`) or `cql2-json` expression (as a `dict`). + +> [!TIP] +> An example integration can be found in [`examples/custom-integration`](https://github.com/developmentseed/stac-auth-proxy/blob/main/examples/custom-integration). + +### Basic Filter Generator + +```py +import dataclasses +from typing import Any + +from cql2 import Expr + + +@dataclasses.dataclass +class ExampleFilter: + async def __call__(self, context: dict[str, Any]) -> str: + return "true" +``` + +> [!TIP] +> Despite being referred to as a _class_, a filter generator could be written as a function. +> +>
+> +> Example +> +> ```py +> from typing import Any +> +> from cql2 import Expr +> +> +> def example_filter(): +> async def example_filter(context: dict[str, Any]) -> str | dict[str, Any]: +> return Expr("true") +> return example_filter +> ``` +> +>
+ +### Complex Filter Generator + +An example of a more complex filter generator where the filter is generated based on the response of an external API: + +```py +import dataclasses +from typing import Any, Literal, Optional + +from httpx import AsyncClient +from stac_auth_proxy.utils.cache import MemoryCache + + +@dataclasses.dataclass +class ApprovedCollectionsFilter: + api_url: str + kind: Literal["item", "collection"] = "item" + client: AsyncClient = dataclasses.field(init=False) + cache: MemoryCache = dataclasses.field(init=False) + + def __post_init__(self): + # We keep the client in the class instance to avoid creating a new client for + # each request, taking advantage of the client's connection pooling. + self.client = AsyncClient(base_url=self.api_url) + self.cache = MemoryCache(ttl=30) + + async def __call__(self, context: dict[str, Any]) -> dict[str, Any]: + token = context["req"]["headers"].get("authorization") + + try: + # Check cache for a previously generated filter + approved_collections = self.cache[token] + except KeyError: + # Lookup approved collections from an external API + approved_collections = await self.lookup(token) + self.cache[token] = approved_collections + + # Build CQL2 filter + return { + "op": "a_containedby", + "args": [ + {"property": "collection" if self.kind == "item" else "id"}, + approved_collections + ], + } + + async def lookup(self, token: Optional[str]) -> list[str]: + # Lookup approved collections from an external API + headers = {"Authorization": f"Bearer {token}"} if token else {} + response = await self.client.get( + f"/get-approved-collections", + headers=headers, + ) + response.raise_for_status() + return response.json()["collections"] +``` + +> [!TIP] +> Filter generation runs for every relevant request. Consider memoizing external API calls to improve performance. + +[^21]: https://github.com/developmentseed/stac-auth-proxy/issues/21 +[^22]: https://github.com/developmentseed/stac-auth-proxy/issues/22 +[^37]: https://github.com/developmentseed/stac-auth-proxy/issues/37 diff --git a/docs/architecture/middleware-stack.md b/docs/architecture/middleware-stack.md new file mode 100644 index 00000000..0a720491 --- /dev/null +++ b/docs/architecture/middleware-stack.md @@ -0,0 +1,39 @@ +# Middleware Stack + +Aside from the actual communication with the upstream STAC API, the majority of the proxy's functionality occurs within a chain of middlewares. Each request passes through this chain, wherein each middleware performs a specific task: + +1. **[`EnforceAuthMiddleware`][stac_auth_proxy.middleware.EnforceAuthMiddleware]** + + - Handles authentication and authorization + - Configurable public/private endpoints + - OIDC integration + - Places auth token payload in request state + +1. **[`Cql2BuildFilterMiddleware`][stac_auth_proxy.middleware.Cql2BuildFilterMiddleware]** + + - Builds CQL2 filters based on request context/state + - Places [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) in request state + +2. **[`Cql2ApplyFilterQueryStringMiddleware`][stac_auth_proxy.middleware.Cql2ApplyFilterQueryStringMiddleware]** + + - Retrieves [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) from request state + - Augments `GET` requests with CQL2 filter by appending to querystring + +3. **[`Cql2ApplyFilterBodyMiddleware`][stac_auth_proxy.middleware.Cql2ApplyFilterBodyMiddleware]** + + - Retrieves [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) from request state + - Augments `` POST`/`PUT`/`PATCH `` requests with CQL2 filter by modifying body + +4. **[`Cql2ValidateResponseBodyMiddleware`][stac_auth_proxy.middleware.Cql2ValidateResponseBodyMiddleware]** + + - Retrieves [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) from request state + - Validates response against CQL2 filter for non-filterable endpoints + +5. **[`OpenApiMiddleware`][stac_auth_proxy.middleware.OpenApiMiddleware]** + + - Modifies OpenAPI specification based on endpoint configuration, adding security requirements + - Only active if `openapi_spec_endpoint` is configured + +6. **[`AddProcessTimeHeaderMiddleware`][stac_auth_proxy.middleware.AddProcessTimeHeaderMiddleware]** + - Adds processing time headers + - Useful for monitoring/debugging diff --git a/docs/assets/ds-symbol-negative-mono.png b/docs/assets/ds-symbol-negative-mono.png new file mode 100644 index 0000000000000000000000000000000000000000..1c5bc3ee8805bee1b0ea5d8f9f6bc066a86be1ac GIT binary patch literal 36420 zcmb@tXIN9+@&~#@C?d@WN(T*CkS2r<0%`yeks=Dx1qsqYq)AKkRU%bU!O#^1=_tK# z6+|Hv=~815PBj0WOJX@}{xSfpA>fH^jSAKpw-~Gr5r7-DDw!t%QZ;3AoqzayJ z-0w&>!F)>@uAcU7+(_%GpMG#I#k5DaT(FyzBC@jc;zxDR+jV}H`Cbh1unC8U0w25a zcg=;8o6Fe)dDpi_8vCnwFlHrvta0y>(zw$OaTfeih^l-v++FB9;EMPHkD&|1TU9K-xSATVwYB+VUkh5>(Cjx5dQT>oHOVYrWxuew{QB%S z7019o&K5Y?`oQ1Mj~rPUeY7j_AwIvg;C*C=JZt=_GfEQHe6lpQfQ3Jm_f8j&&PCOx9gPa!Z zTF%hUh`_g6{t$Ew(#M`R3rv|C4H_w2tD`Ls=+%x|D?B}E_*fqM%oG)w z#rjO*cI!{$Sg}))pO4?Z-12ny@*DOeNo^XpRo;Bncyav2;iT!ktvw$b_1e+;#n56m zn+eO}>4~3l>$U?Ms~>fv20O!_P{?U_|1w~(VwBinb%{9i+!qh3WFi})gV}TY0?fn5E!U| z2Qa?lYAl5(0FUydAeAK_`!HcgIza@DjaPiT|2EGQu%TZv%F(26!?hg9v@ zhd9#?y^y8ZF-~2VmI83(ei+85icx1XHC65cz70=%{QF*@A86P$06$(VJ=0vf8;wgroc7w;V=8gKXh?t8wWAP5;%ZI=>SG|U z&xzsSH^~Fi%T`75FPt_WyH)hBdW_ z4J$r~E`(vjjv)OXS$}+JwFs`?HEeXA3j^U%{95h81v;;i{f&Ypmj`VA<*E71e>l4T z@A52(50`IGNy8s(0?=Sxu1_Tvhx_lF5y}u9`)f_mG5S6zBpz&|pu-N)8RC(H=;Z!? zeRy)z1op5|J`R)-FlSgZj29*}-W!P!*RMZBBGDgk)NopGS#YYL0z)bRjJ?v%^!Hv8 zc(F}KU~WRC-$S_Q0;s4FvnFCBv!_U0e+G|%;KB;yX~Qj(H6!^RV$p!1F~ZUIVRA1QqaICXtOr`B!>uO_U&CT<>G z^42(G7O!wC1@TlNCJgC*6jdoZ|BzJ8(9C*%M``ROP*QKY&^59pJJy;OGDq%fdLJw4 zg^%f*j;kVpaG|Hr6@rrla)`zfzkLkOGJZjv5QXoL?lSc==>5W(&!lR$B?-#HT6q@hJwb0~Zz1m+*pZqnF5J{Z zyw}fk34IPa)BegP*6jW|eF*v02G|8gp3w*sSmLGJ=gG>D5`NNTH(;|GRMZx3t z4V=JRDU$gi{RjDY4m9<#53__UhdLtg@}m|b=Jgn2vb=9-WK_#KQb{=HRQZA|Y=w7p zwwl5`CyxkiqDtJkV?a~_=WOazL|bhU&Z<+G`vbF_(P&Wi_Qw?+MJ4Js-GcYgcPvZYAUp&Lufr z+Kr73nA=$`t*nkX=LfS%M(5w(6J6Q`x~}r@_LLgzg+Jo<=W=|)rxV)oe`Q(vRV}b7 zkOnK?TJld0f|v8u4NUY_EisV4M9y2hS-Hz~K$@piZa8jd#ctaO#H(I3u**LkB0MOq zd|#M6@_RFdER#Qxs3qmJJ4sN6XiVh%-YndTvwbmf^L*+<+|;N< z!=7DvhtCNY7dp!7RvdviZlqq5C?S+@?^5R)LDqSyz<`LRW)oF-0{NCkU2RJB^$n58LLx}{WwBLG$^_vgpnpfiA$0?>doI=LM5*vE~ zUq$S7H49)n8Q@0mnR9iLBa~J4gC%4(1p1H!UyN`0+t9PczFl##hCn1%+dwaGdPNnZWb6~K=CE1<|yu?mKE*YDAPw=4KuAyU)@p_ zM<>;VZaw0NmDVr4%UvSgKF4gIO025MWgu0vE+HukM?|#H=&8DY#DTB63i;<7yIERu z%pFBdE8fh~IJ&N9*#9)^hYAvGXoVWA{KkOtkz0Bb87KOR)G7G7UlC+{>XQA*_XZ%N zJKF$_-J`CTx(@?yqi?W|p?sW-PyKRxDo5ffi$GsHPs_tJErJ>}vq`gHJ51RpE0Ve_+PwZ8eYdJ^C1JXNn7vS3URJ2GSVMi6f^9JnPr z{{SRB{suXtnoJhrpIh_3lansa_3FFH_k+=U* z;d;6I3Z1PV?K$;c9(D-p>&rh9ou_0nH4dSakwyra0t?@Fo-AoYQ*GfcqI6Y2g2-xF zljww-2)EM(0V)3rA2rjrTHO78(g329*;62Mp{q^}-7%A~U@P4FBCGZ+(AjPe291Wd zmgx+6o|kOms0lxxVVh(L56|=qCB-p_-l@G4#PvDWW8h^L3B*JfPl0iE_5k;;(00Cy zb!Rc@WDRZ*FAA`Pn>3_1TKcN~4n=nIn;A%S_&B#IIz@J>I;7i@jIt zjg^Adk^8E;qnbPCN&>JW-6ngZ>Uom-0dzM}ee>~5<@ybuv)*pa&I50UOf$4hM)c3R z6d~77$60yU;AD_T6NZm!=aRKJEB z7*hIy z+fp$6c$V<6#?wI#sATofa{6jNa_(UE6fE_8E`t~w_T!FfC6^O+{6-_h+vy2m-~7-=`HYE{ zkg!Vf$h{XVXsQ||*skL3w{yxd`o;2QK70 z_PTfTN_6Zk3MSGn%}CR6%!%Ty_i;v~$<5&GDUy!;?w>qh!a23oaFb!bQM|Y`UE{v= z=s+qci?d%=1)<^SO)?Fk9BWd_s+Z7`xqcbfku&Hm{U2$U-TT%x@x-d=<+!O4=cXiO z-OU|DB&-0A27Vs9Z$C?jW#F_7-Ur&J}r%uxzGN)M-%^0j5-5kcpzQl zsU434IZxKI-+T7oW|SehG`ooJ1GyVcqA(YJ0swrJYrY1dpA#k4q^diz-gNm_PKvr>X7IV&aHrK#2^pv<1fkZ*}7Q>SZ3Se7bQ7_&52hr{*jodN&%HR>{JoTImPC-ioAW$}_N1vi_= z30(J#1I&tOiM_#quRJ|-*)ST*u4DAHsB)`M#$;hFZ>&ji-iBV2pK!16OOTP??*u2m zW+96i0DcKO!kmKna>xzAC9q5qjWH>XoaOscNuz*d7XiGFX=u&;Ep<2YwB9I7mp`Ne z(z>y`?ay8+U+@~DtGEJvOC$3(tq!KU(bJUz%c~yDcq{xyg@u<87xPp|L7uo@Gkv>1 zx0#Qji?RmUjQbt}=8Fk}E$@^g9a+3rIxhg;6+jsa5K5eFY`Mu?b`vL~KERd8`(W>O z^nKrQDA1hEZ-%-ZuK&;;ps?+pMp{+RJ9ia&#WfPp-y>eZLT62`=~14KFUotI?h0eZLiH?;yz~l~O3Pp>97g+S1p@ z61!XWQ^0SC$Kw6$N%4Q9yIJw;1tgy$wENHURT0aiN@W#)ZtHz9m$P(BE0#gRiTGhj zO?6X(SeT~Nf!nTmpG)J*y8ZEY?2oub9)lcXjbBIMQT)x9~0h4r%B6IAm1bT98u|!F}aIvL`mXHPvsrsOIXVK1o`Q*3pV$_UtaGZaqn~K0MCm{Ban`FMu0FTUn zMFH+Vm!GSH>{N3`1jKr~;-xz;p|c$;sg7cAE*t-00~ccfA%KaZ3;((|Y2n zPCIhp`ukq@HvGw-zl}ioZUOjBp!e755C)|#B2SkC*KPn`aoeRGOu6nu)?5Kj^C8_T zrq)hCDV!skEuA+GLLk%h{>a|wXN@JVrQweQ4<{f+$9VBIiQh&Ug)*1h5L3zTfS?r2 z=VD>?FM*-InC<^dr;xJ%p#8S9W7+q*C(`!Laws$&O|*-XvlXS^&P(X{#dat&BwmGz z(mqdrAt5$Jyyww04Y}lSUb@;?&;itwT}NP>)?ixlXFW+4{M|yDd24tbvr6@h#km`6 z6}R~eUE~Sw~t}cdrRN>;2?_K4slDh+;mrSqI_iQ;ysWL%*U;?HyVN-8)g<4k*+vtBS)O?Xa`;Rrn0JD4FZf8D{4u%FDjQqs$cZsDCja4elw-`a`pgSgd z+?=18XY<2aGYZuX%5KP)-1`dHTcWd9wDVF>e`m&+vaPdUT}gN=Mwf^lwEYX&3r0b{ zau}6}h>%?|2t8L*Ci#C4*Vp!})&h`?Howw$#=QP@fB)_>y9Ra{f#zBol^AQeD0ctb zes&K$NMR8&*>@~P8npfIcfnS+wd1fCpSHcrMcf!iRRB2r?kYNC4d)s0cO|;m@P_K1 zZ)F!)rF!TtLxwJ-*K=~pfFHLn3N?(DvT+I_y9rvqoKkjpM z14q%R#hjNyG7yP^V$`Xko!{Wv{GZo7*-t{hIe}E%9|1_C7LBva3iAJZ2()|yeSb#ONf{+_M^7PT;#;A|jk~g4g$PVQ5YvITL1TQ7=0mf!x z5%wx0#m8M7=$oqN`;X$M=$?lAGkw|3w}4@1I{*LqZ}tD@zdp?NDnR&?kAWG~Ot8b= zRjd}>P5*A6g5cu>oc(t{=&aEHXe{$}{b%!Mq#ty|-+Cga^!^Fw+OWWNAU)@6xb*X) zFp$89HvDj{=LLb!8Y2fZ81n<@lDOCoa9nb<;2adT;0Tm2k=)PdUn=0V2Hq#>Wm~wo zA&)bpa62D)3Q3D5h7y7qh7l16UcVo&6c{eL(SXJq@*we#J#{F5$st#z4aVSoAtPH8Q9fw9(+YorzU#^MkFZ{w$GI8G&|kkp0maI7YfT{(Kgt=~ zrY&p0? zdzSUY0qRn?$)uu;Nw@R^^=5R*iKPpa4>bZkW~-AQ#&5i>ayO=HkbR&hQv)z5q*6MS z*p+A|48my~G0sFtbo(S_yC$i}%reYHuRrga4DFeM4?Fa^RH{UCNsY27ydiHEfQ;=R zE0gcJEm`X{t)Z5WMjrEa_ZW2%P-W|~^Mn%qU2xBfG6_G&`>HQEeH}Ykr-fIX0DZ7;}+; z3}Tf8niEmM&>>ojF=Rt(hi|Q@XjCGKnZ~$;x_o3!W%;4UUTl$qZn8`jh_Tqj8`qZ^ zDUzg{ZCpFVssn>ZH@PT})xPv%G0@*cguR6fq5`RDv^*a^8EV@tj)AT`_P}2w)ao?N zO7y>>HT1+jc5#b5WZahBxKfs}f#70juxNReWI-2`d&xT_$w6t?nFm2x7_AUn1N81{ zaBjNeWCT&uCS-P@0S|+RWlT$bWb6DQgct~?GH@GSa8|$^=vS9r2lRJ529ClzBL|=s zh-;xtEO(k6dhQee8iUp+(M3;)d-(e0sxFgruh5JmrGNrjj4B3yCZzW&7J>7oL$>FS zD!Gmo5=61qF%RZUq-Bi(5z?F)^rSV$HPKgJ2fkjb>iVv6VAB#emfvw+j5^I@5Tj{4 zT?Jy1b~rz@J!@t^Ox$GB#w{?`l@2Fl-8u_h$>v2a-V|rteMIem;@E`Sr$85Tb@5GVNhaOFzg@x7!Tiz15QxCzOw%HcsZEk5C0oMBn!|^vd0 zFrd?P#zy(IdK-D|^n!)Sh9~2?g!9$Krz9%|8Vh+gOVTAH1@Veg&uOZZm!<^ir& zB1lt{`FoRR`jnf$AaeXGnIoA`1HI3vA+Q<9J&`}K@ZAqt1C)9LbPst^~Zs6FsSiELBFS`LW|u&yCHxPu^e5G>RNyvx@aY487=1)yr; z_d&IxQieg~2&&O~S6P1b3EWsY9>! zyG#zclGBF=h3(Q4p~uh->>7+J`Q=5AUCu?4@H>jvK@Y~rr)gB*u+LbwpzlB6?Ci78 z(K{PIH_}-&%A3Qoo&cq>bz`vWAjc&BB@n9)52jL0-&+m4y5sv_zhb@PjfW{0kjt#r&b^jv|U8(dteE~i!TNO!ULisS3a4aSF zs|ltBBkhtsZa7&_(s+yC4hqeQ#C&rExFEHIix6oRwl}K9BPL8$bB{7Z9*iLp7-yTX zLnvOz>gV!{(woU>M)$GX&+hszVerYx1)2T8=vuDfhd?^+^3;jNCc=QSJIU$u0u5?qc&=O=p_jo%Xg~c0` z+{xw_m@~59cD#e7^`Cb_8IMGmQ%EF9>G_8!p9@=Cq6%YuXA5=YxR;)ujKVfmo>pR^ zz5k}>8iRrM9HBe?43r-zpAJ*a4N>h(On!IS6Y@Rj_ea;9>^D~A^KQNzZ>>8L=rLi# z(Q}gJvqJO@_{k0al1{j=}nSp!4Ta=H~s#9p#%@b*G(KDEWr{CXy*kzqyo?m1c!~ z1Gl&+NMJX89Z&{oj#Vg;_0jSo5Ou17v!qO!w zLv!i)0U!6>9I=I;3ohs+U1o-)zm*cWUXH_3*Rj@|8muY@stUh-VZMWmJk<4@O$R%2 z2{qQxeOp+ul^?%<7E+iP;F;a`N}s<-Z>`@}m)7rTfiix~an-r~bExDPdW=cqU{%)l zj>pazO2JBmr6ZckY|mxQsbB^?c?2ROYyb+@ZL+>9t9}6azLyV)Pnc39d*rQiqI@)) zCC;Bx>i+KS!4V>ZNp$;giJ!rrrJ*0}{fReie~A-+|HM)XadXmC>s63fWcs;y0TA@9 z>rP;}geD1h4Yz#>ywHI1_g?LJcA<1qo5Ic8&kilEot%B=Pv08@t*I%XJ6=sVk24kw z-o|v7VW2xjV~HeLkx2saD{)h$#a%{J%E(agk8@1ccYa7AG85yQZUU6i4mr%QSlsyb<9+_6sqCm1$0^@n<>aEb zmcWaDhK%E)@Ox;7q5w`|W(Bp2XTCuh-x)#TH{$n;MGT~BNJ5T@J0rJH5~l>jORs7AbONJ| zv77nlwSACt_UN<@I|VRxxYZozS;{r%~n|LbRag}BRcBtFjq zrGmjMT21~WR`CsTZh9Bma18j6er!4^NBx8MQnoR0^bzyc8mR~C7nX>8lMbIo3G>OW zy4|V#kXc_6TurL>r)Ol@#6Z<0DN2o{x?tL)(lf(~HmA`=Z>vh?)eQ$sL09qbLk!es z1J(HyiFS~K;NfI9y>2QyB+#M@LPS123De}z5ZcuD02Xu*<|l1E&1RE6%+N4=Px?BQ zU0X{9Y1|&eGrc!1{|U3hkgVJrm&Fg#*dl4dHF@8#J_MR-4vP^SU1wciEnJn$|KQW} zs5Mx?|Kp)9y$p$xivfuP5nX) zlv@g+JRSnRtehZw-NsTeQs)A^kyP~dnF_M2{(7yaea2N}tjIE{o?s~WTc=x!9m;F- z&Ar9pj1}T?=h$_ai+$|oNJ$~Php>JEXGjA}RehN=f{SRPqmyuyp2}kQ&u>~`JF6HWTE;{kT6(fn7X2h@m; zvLMC21}DLr*c60Nm#(z*GeJV_Bs)B^YlMxi3pkCbd@~pzo(pdDiY~&DJ?=M(9ZT-& zcSW4ejtMt!d4=?~z-cyu4%#w;66(?09i4*s4K_B|@TDw8c8XX#i^c0YL&iNy$g9Px z3dzLHF%K*MQg@d)u-|E7BE;FaAIJZ1{Yw44XLG; z(sX?+{>kFxhk6t0@m#&t&&sV2-(zfcIUHW#HiD?)Z9X}BBw2}5h=R}4Nt5m#wPT0TfbE~rt zY%NS*YJg9edpXJOex#H6Mt&*3ejgvyTS5Cq=IuLo2W!nVhp6MMs$Wov)OE{=fHloLE;v+*4>aBY`VP&OiusYG_Yh^nX>7-Xp6ap1HX5vg%#g8R(l~<-8*QRY=3ey5 zCWNw_`>Sq0OWd>qZXkEp$h2Vkx-Tb@C|2v~Fm!ur1&*OJsRdO?7%>ap#Er34@z@=X z_~iu$5Eft3RJG7WOkc@c;IgjFAah;lU674(Ms5YOwMuYA$HyMpOGwf?$c8Vv|tHTj;u7C=oy}iJ6A&ggdjH?${M|_UbcOnrvK@$*HoM z9_EqVPsBE*HXM^_-2L&A8h?ovCZLiC<+=$nI6L7SWv3^%H}#B1tX04HGsfP@G{%mw zYcOpZ3a(|p(6X<+1yz;CVVfRZ|JRxBE3Rj5TeAtV%&`~nqG(4^>Cwa}=fJITb`>@i zB+WAk^mf)he#T#Gb!pap(H1Sjn$l|)lNK0+Ybc1hF!{}(*7VAe{ILC}^N;oL`)z%2 z@)x0rZ-K=+=_41G4QV}uV4bEg)biws_NCiXqnOoDo>^Le%X=e5GL+FU_=IY`^JV{v zZW9;+b>2VKT(+LIJx(5ZPq#;_ND>ssQaJwD@UesRGYmvq=_gj*CCiOS_SQ2taMtly zsH4*kqfY5H9p0#+kA7BbHA^iLs&0o+L4_xDb%7Y@(W4{3>cgY=sOZPN!dPo24KzAz zK=(aaOusdtC)+$vAe(3-hd z(-}I*60^EuH^gDLKkP5ooG8Z?NsQ2Ks-4n7JMzBiAn=Dv zXeLpuC-nawqimi`ye@6#$rp_BJXn;B$Ov6P;Ju*q;#(aa_3a?z$ZK5G7a;ge&-9p`8q7Q0gcNyR)W8w~O z0J+Gh6io!zz$2L&DI_kIj?6)^5>MnF{X#xddYw>;S=IF5i6X8N3h~7iCdE#oZDi@; z(;(wTih5-B^PGjsypj?j8>3L=oZ8(S{sF{dM&L57*u==CmE0%4n#r>>F4Bj6D8LM) zUl@Z`N!7Z7&8>}B(ht<-$K02;M5riY7W+)=%@HqUxw?gwzFx_h99C?{K#N3TW3>7u z+}K0vqDkMh8W@b?r9BjL__bp$zsd?55qOa%zTpBO+k8wxz<)?TT3rsKe5|lS*!TQ1 zHA=1(x6l3S%gaKrBaCx~KF^-!vDr@)CBKsIQA2u5ETtuVxqq<4-O$mnHhFU(iUz8^ ze0uSu{M%C|aCM+WGRUGink(R(R<}tj6zbHq5s}^53fBsIE6O{v`|fZ$v5GO!w#P3U z--aJjEW|(&9sY8?MC<))am5T=McY&^W07nhR7rSYD!rI35PuFYHXIlPa;Pv)jR(}3 zzxvAzlyN$eAW1a4=EscpHw0x+S%Oi&oG}>B)@X=*pFOBW;AK#MSy>oYpfR+1De}h(7BZ1u zk;Gv)f!SPGUNJ^7Mw1Q9NQ&m)=W)Y%vg{rRbOQX*tB2UdHuC7O6d#HWO`Qkjn(^U+ z(Wg#?&KJ&p%4g7?$TU{#v3rHAlkD2TH*niwGPc$@p)V<$c{K3>3w{PjOQqyLxTOsv?FNZ~?PAyCQ>} zDn;`{9D@^oA=n=CUWAuxofR=kmVOPiFNUjA^*K3VG@bUHMhU^-NqwFOO@2| zSeImkO4*Fs>Iv_y&XJLWb^L)AAQzZ9M%p?{35k@w5t80>X&0IK8pQA87_0GnM?u+C z&*Vx_TxihMhy)hRVcn-^?3B{Dc8mU4a({8so`03ikN3Kk!e$pmw&ogaC|W)_Sd5R* zYYNwEYQN6>mF$FBHRP$>(yRhFDuOyGV{rFRljo>qo5)ba_*`CufIIW9m*68bHJvBt z%=PV`%5T}Be~Dgn-Cw8uQH)j0y~-ehLh)V~&PXZT6ImM9cH5xdSpnxPVf&nvYID!j z^X@2XV7U3YUi(IJh$o%a1&ueJJU|Vvw6VkHU~XP9Zi}4Mk}M8NjLhp#h;I4pPf4rt ztM-%#9?W>R1z<~DluS;o1)bnZ-6jH0F!FJYEZsO}N497zAz*atqBC@-rHm1OaaKqQ zOwU#n`I1>zck#dL{Kxo%}@D79;s_PiKaTj6L`tmxy&~& zPp>1T#_zpeiTJq>+a(t%BIt5HKB4Wb*A(C?vMQk%r!3LH?5;YcJDw_W)$RUm4;y_T zWY7(!4l+5|{|6SEqPH9~XjB7;N#g@5O~4g~(Y9B&kYJ^bF$c3k5@$KUguI76xkdTx zR9C%l;03JN_5(4D7{-n*1zBM0=6skLGR)>6T*LT2l1VB_BWCo;y{P( z3gx$wD@TeRLCLGz8EP=!sHBlpxdvZ%$gwQA+Q$(CLxb41zqWG!&}0&dU376JhL-j{S(RAfGQWmZ>6&XpqMVM!z#G78if)yMn&x^M$A65ZeJm2;1}o&Lp(eE;uoE zek13Uo4l(t`Z@2>D{vaAy?Qc;nNpAP2}Vw6RuJSOrWE0*HbJW1jiQZkVQ;19!Wa6) zo{{`)8PRLs3*oD%?@>`Tt%8_FZ8<+pMsoyLK@G`+hoqYBus!Tsv^b-VdT#?l{0DMf zELFTff0fIAk66{vaUSFQ*CjyKRcis>_hB426b#4i*Sl-GH89oFK$r&fx za6X2pWkO~&+kgH2gCaf?gGmAzccrlulR&p=ZpB|}=VI0wJKU)?o%M@;jk}2=AAjw8 zRVdIH{Y6$M!?`16HA1$`XX*Wi{A)s7Ls1jGpr8?%j2R#Fll3iQ>1s92BW|iLfgr*` zwj@fS4zkVZ{vEA%6-8Mnpmv4}Y#~L~xbA9t zPD=S6L7hSlBf@8D65m?9q~7WJLK`Czncy%UXV+OfrP&$+ zt5UA4)FPmviYAv8tf;^W=S6r>PLzXma}bq^m$*GN+L&0i#WCfTc&S zm5t8=ih&wSULHJ=L<2%%rHPxiwMBO4M-NK1tr+~^cv@z&rl=K+ol7*2 zF9r_`y)=7<(C(S>AZ2Mm(dgq6iN&tA;#*>R=$_aN)JuJ=m0wMx1LjNMsxWbb zlokR0iu&VSwg*#sDVuRwn+#5-HY{_8QFa9@V5C`lxsev-I)6D2Kz63zDXgOIz&mp^ zH3#n3mi3ggC7-+FAG1v5WC$B|sBgH-Z;UzV!b~CN&Gt>-iBF_iU-bDnP?@J;9Y(sx zSz=yjqpyg8EzP(x3+*1?Sq!L{eU4^9PNp)Jtzc8{jN%F7QSA@ z>S>Kl;i}7wu{;Ov&zO@=%#`26s_fj4ZIjaVY8tOknj_+(h@xfyFJ(jnhr5ZJx#>lA zs5`a(6lUXh%@fj#{YYigko`?+8aJ?Ch~H- zTgi2;cL*esz%DcN4?yWMvR!W7S<1l0R3}aco|XTB}iL;{DJdFmpJ=(Q;^)?X%VXs-b-%_#^=UyLBKkF}!nY z01JlA`s!3M>A;Eknv5M;GOekWv^S)zc^ru%Cg1Vtf9;sYXqJAw0~+BBscXJ}@gQW! zL4|A18YcG;eBCCCNN`i@?_H? zB+QTU35r@{UJej$4n0u&vNT-`%#tTM7Tg4;-U$wQ=Ej>r9*tL`Jd-wv;I0K5L>nTa z+=7d8ETcU!vk0In0vtl2^sMBD2FGzI1u+L){EeV0>!LuV&m(hEFH z?nP{h&Q1W6h9suaGv6XeiYf^(J-&hHb-WV{?K7nGIMj0x8q&kogD5VO^6)b!;I?`3 z(!j0GNFebF4Z;L5P^hAFGR)BE@qRAY%CkDlvER7J~-DA15MWa1~$IAUrtbt4_uagDzw7?-pf=*nZkT=fY z#P}en)nj*ARjL5KgLS*>rhSDMe0nyFL(16~J!ae>6~Vx^Cqvf7Irr#T(E?PTo>I&K_Kx2pI61Ic1RL<`@7;40-2%Qo05nerztQ`W!j7mU`iQ>m*U#U^8$PU zPg>hMp8H>1`T*|%6@(v`uuzyO*M$4VsxoWXq@lDUC_6!CkX~ynjor75e+DgZz=qt0b|`or zC-2Km5r@?=S@A%emRJ}Cq`AJevMf$9P~_TcB9GKe$xTzswI{_GgYUEz3G$U3XT%pJ z&Nn+Dsak!0s=!;`y0XsDk1T=xb)-VyyK-(arhAe0X6mp-PAb+V*gyQtOOw9N9j zwJKpIew9SqzKnTjiJS6gGLiiy-NiWn`=wu!HaLGlE2zcK zThP;^)$6zI)pL5Yg$Fe%AN%A5Dti0smCydtrF<{EASXFsZX>)`ZJdmt$imOv)aHMm z`#Z7EkXs3$3Rq|61h-9}7jE}x+?(kFkAro138&FA2u`nBPb3(4vq^8L(wTGaEOAF<}PXzd%qN-NAx_{OrjuM^KB5wu)0{ zi{E7?n&>ofz~T2yYIQdl7r6`ZcP5NGa9Co6ICg`ZTc#1Rx$jd|B{wj22dfIBwiSIu z;XC~^HG~r?lIWs^s)+1FS_(s5Q7l2;H~5R)gVTmbkoaEFxVKd)u1jd@L)h(%W<$97 zd3uRW=!edH67Vib!TzK0;d`OmTnd6v1IwKH!c7|eBYbZu%04f>50qY08j`A zc3_kG_*y~Y^rrjdEP{p?Tb9o0<~4=;WQAF>ZC5;)NxnFJyv&$*`SR<&_@kFAE>%eByGlu5 zqb8kQP;WL_C7xYo^GGOI-QV-+@UFGl2(@cdoeu7(9_wsz6q#x$nhj}OxTBX~vzBG0(lP+lkrExh1Gh%(>*vEr2zPrWxA z0UvpS`TKbmsxhx*pu8al%KIJAFGUy}8T@q|wcP$j;CUHh>DikhjG!)^hKKp~xM}H) z6|}uwjaKfeQ+$G??Dx%;I#I-nGZ)h}7`mABORqIv%$$+mbM~>Xcrc{Krc#$t@l-)? z3}>=MejLvRm2Rt7=tMnmju+-xkCBnpS37tJ)rhYB0d8HFl1}*%ncoe63=T&Y=?kOH zd;g~A<2Qaa-!tK4=$9%vSNi%F16;6w63KhXQr_<=8rxI~FWf%0c=h&l{KUP9LzLy- zVUli{P(Foh)-JCI zrUpP-sgz9Fmu03*&%W@W!kJ?)lU{S#vYdTia|H4u?IRz?#qM$hUc~O^6VHM{Q^cuf z=MKr0BC4UYZm}^0*|(e~FLsY*C^IyYD4BkIOezAK1~Z4wc_YrgS33f6L0SgF4*#6* z*VWMX?;6QG00wxeOE(< zP=t`JH1@5sWF2jkBoi_uDkUMZf2>a-$w=9kFsXkUcd&q>O%~|_+`u4Z8G(iYAf2dtkl)(`rZIercKDG6=bT=Qs!Zo1U`yCCG;bWZT zOUz1LCjdOInx~ezPVF(g9S4}`@Ur$sIib+THRsJE?56Y79!L9U3SG@6XL4~=O|zYt zZ4PQ9{=Cr>^3hnD*>&a8YgOO~j|XDn2|bgnlmoax;=nz<3p&`s#zSza@Au~Ex?7~^4OI_tS9;amg*H+BiulKe4z^@a6-+() ze10KVsMwO6p=pxda-%*?&aIDIvrPiin(foc%x4_?QTv?5jG#YkQFKib+pqwXh#n#} zImdG!@Kj`0Jsnahy$i4~u64X~5bQuQ%V^GzId$59%i@~)1&!o&aqf!Xj2Ce!+1yw- z#9Mufty&JEplg0$1BG~@Is9XEFlt+Q?B^gKz~h0X z(bND%mz3^KIwP-P%1p&`QMe>PCq=DZ@UjlF+KG&dtA478sS8%*k3NfyrUwII3Jxlf2#v^ zIkmrj;Inso`!)J}7_+$h;2luvj@8v2dv0HV0R=KG)azCgX;eXUh)pb-*%=o~8B4VG zdz!mr3vHyR1d%(_;h{EE=xt-Yf zPPB9o^~haY=qnba#aTS;-*JyQ^d^$n2p}PnDUqjgxnV&%VHO?5vuUAw;y;1Ejc;|+ zHCxnsy}Qba(a#m3{nFtyrxOI91d5 z^xuqol@JF?i7e_~CXVWR!F}Y`B>Xq;c=xF(G|)WfN1KG+cjiH%!l9Q*o3DS=Yh|L& zQzQdWF1_aYDEFR8&$Vp)m>?Y0?IV0j_t@zPG1FRZX0v4x>gDf6@n7d4U>@~79LUSr zK{+{4#3{?#fShN_F-wmhcP3iJG~{p}0cCWYRmws4j)aQA@y|t3p{$24ZZNLh9Wh^B zzy$`=#m0GL?;R@TA%-7B#s=TWQ9!tPyR0raJa>W{Jr(-$!3oGo{vs;CjI>E~-Qg9| zKnLJ@rmvJM%$2aOA1QqV=r1^fg>k<-{VG#MMg7RIp@i|&xjkRAHZ z`*BszpVkMXZ3^gxFH#b|p<$n@ldOvO%iN|8D1`8x8oZqsIkyY+8Zqgf9mWOPKCS=lVd|3EeUNc0 z1%T^Y>7jrpIU1I1p7{|OElN3`j=Vcc6F4dMRLYXSp*(us`H0?@n&}5*3xoSsEvNP< ztXxDG$Y@SE`{XMewc3BjPKYdWs+6~`{Z;}p;gS4Q^h2ZlGJ^--mZ_R5ohH z-bDx7sCEC;G~~13k{m9pSdTYEYPePGhpQ|H9-L=!F&bd=Qn{IvLKU-|I4ly3z;|fc zE%wH9ZK3jV{Fn^$ez(S@{iqK)`%q~~*&WrI&}ev?q#!L>^9Efqgsa$))IGyD^!|bJ z?kfJXT0wj+BXOaGzRHdJk5-PMfRjgj>~>!K&qH~;J^ug z^TgSne?qYwub+9Eqmj2fE?zRJcRYF_Ye;Z(Go$Ak2Sm)c7)`DLhqHW$(j$wZi!jE4 zyV7!ZAlf);Bug`vei0DTfN`#=y-rYEMQaumjSu3k<<6d3+o?R!R3!JO^mQb3uCg~zQDP$OTR}8TAS26AVXcr`Q%FJxQue4JS#L&-Mc66 zdf8{!+Z)ajS!{X*ulA$Vi^jI6rY>@>t@D;v2ZGNsr&%HngLQAmWno4w_PHJ{ro7v> z%D=?Id=asaUnjQAV=Dj@xA68;{4Q9H=zVWJbnCkjCm||m!oI%?ZA`;BZ#d0okoAy_pfIfy8 zlC)2qb}mmTlK8tQUMh^N#yh~ddTwfuFR+Ht52dG~GF|0=5!#J{CZI?5r}cs=Wr|Iv z_E473g9!^n#2=J70y2Bxr2s$ToG=@}!#@0r3${!e`?@C@5M#a!kPf|trILlonj{Cp ze(b*YL6CL(jvlx#UcW3F{Mg6si{K(?91Sz7RWhgV7yoC)=9zTn8Pr(Co&9QUC)c*y zVt9u8BpDz=69I7DSrPSt*c<#3-wsj15dxpDY)AXmHIL+7P8;Y4D77mJ#w?qNJvFqv zi!PA%?BCuhI?uu^V_)~UHbebs6??e!4cCwwM3j5~P}29lCP{=Kj7|GAH=LcKgPv1k zj?p$=)gy^zXW*#$ z__0dKiqT!PUa1YWw+ydOSSp=s>M+^iIro<Kx=bE*nA`r}2 zQCQygUmaN%xgDt*Pnac=G!6tAwjef*GZWtj#--=TIjat+^&>5 zgrF|2U(0C+>{Z+-`|DYc1e8qN*=fNAhU}hEHPWqKdDE@ID9}Sda`FLHh?Z(BmUA0F zMq!3ahIB38gfH?z?#Z7iU9|#d)c`_%Cn8`OdJ12qeN4M`g!7{lNAaQ0Q(80 z<1sTT_j34KgLjUUvLa`-PG_q!?$pk^u6&CA3@Vg|K5%Pf)WOT2?(X96cj6VJ&WbUF zJECv57kK4ka?2wo=eK-zO5ME(5IqnreAiO^^#P=o4RFF&ANUao9E zR8}hXpAx0maDsEBe&l1K$sD=dorhyny8T2rs82sZp?g*h417UyCmfM?Jk9P1vZ9G8 zYQp$$gP|iZOQDCUpRXXF8bP1##ksS4x*M3(mOSAmhI$eB}mA+O2 z!BaNC1=*)O0&g}{gjMHW0L2gPrXHncUDP}2J9^#m(J?D%4ur0n5kVUCX4m24oj(9Y zgYqzR)Y*ViUs>o*+Ro0d09VwEGA-x7m_+2-Xhskd^jDTS*L_g<$7O8g1e$r1LTb8z zE$agHbA@(-=l^%L1TCB<`-97GH-4-IF2}0=cO|~!qQX;1y_Bjd3>p@~E#M+9y#$P# zQcX>VPM|L;lfHnX*zMBxK+S=UM20M$FLh#?5gGWuuhMzcojXOrIbdtP@MnXop8B+F zZ#k$$iDQO8JkvL4hg*;f<}Wg1^&q6K8WdOE=5HAf@En2CU;YvH2lYCdRXsOb-fU5^ zK^eboV@$i1q^vd9ijej>gtY&dKP>Z_ z+7o0mcL*+_3CO=JJj3ePa+m>PAMElE0O_Q(0n+IiKEr&sNwO3fGK4e1jEgtn>p!CJ zk9F93u#l~&*O6oYML0_8EJj_%`KzZGsp%v$?pnI{x^ngsYt`~^-26#ba{5nSYRKqO z5j3*0Ot8dI`seqmn5s%@u&@KDFy$`L*5@VO`PfNV+x-=&KB@%dFb^Ib@*D zaGJC!s7`??!nWX89A^tV+G3X`cepvD0@CR!Cyj-x*X~gwL(w2vpE##j(s)V0T<8PU z=k-PyZvNENfglM_;Q;Ys{VcOhRlIQdp);LEaK}b%L{D5*%?ItEhqIfP+2&~7c8mi1 z{V1^ZBUlo?gbRd<33dhnM-CT+-|Bm739J3}6GCNF(8gW{`wWtIZ@nD5a! zWBt}GbRDOx5XmkKFc?M@0>rCW>ICxk44VQ$Eh{T*OO2NLs8*mW;kf`%CWYU|K2q1` zHFIG5JS&iW+!lg1XgalrOAr1~fhb$_-P8MV&a;azax`LmTW|GOzH1KV-!`e(4^(D@ zDN^SFKOX=zUn~{{(hk}8-ohh#hY<+=~Ns+!C<*59xXv&VH z&5Hv`*A;Hfa4wbY4yOi-c}LbBhZlV!Z?jh>RjhxPLutFuoi(}-Ru`J9M&#VTDZHbY z5ll;R8+Yd^U})lXG4i0+BSJ7VcrLC`jRzQYQQYsmKuO^c(KQseEK~DUp9;)K&soOX2fCG?0_LB$V#?Ne?|?Fs3yCQ1eGjTR(BC z`#o)JtmelN7f#0%es~-l%^&(SuEJqlj@Apb0x8|TMt9p3sv zJ3vfZ4Po*ayB&PhQyE_dZX!1Jo^N-HrpaL5Q}j;u)}y zsaE_e--Ut0a9?K30)887iu4!8Cw^jik+L@N#+FuZvBGvvXSe)b+>Xo<%S$u?Y*b6Y zcBE>l;(t&vUWYY8NWJ={$g7CQ%V*vza=&<>Gikr~tXEUw)%afxtbed0Vz?a#iZm@1 zb&st=t~4usUANeb#5gHNz>>8-Z{<}fbqMlEOM9{pU}F9JuK)`~Ep=a>03nl_TS*^a zIR~8;+Amo{2ZlRWaOm z;v_6%jSueB{M~anTu^*e4eu40ot|Sx%~}p<9l_1_l>F_E$@v*m(`sbh&Ug=8;vW~q zORkQC-lYweu|f)?QvO&M1(zAzPT}wN*)7-!7idV&{IO9d&M9iFYUsU>!Q$kwY;5XC zUOzZ2b|A!tG<`bwv7hle%V*Os6<0``Zwj=74ov=b(~6?g6M)iT{cRL}1JC2;XS-dc z@SaX3jb%oHtU&`>+gF%yNwkt=p@Hh(xmQOw@{$^8g>3t(FY^-WvV2hMB)|ALd(j-J zN&D(ibeZaZZ8U$;In!I0+mq=_z%^q%ndQdeU~wp1*7VEY@?EA25eCJI1MMy6Z{xR; zd`ucw+NKFRK@Gg>>}Ovx>b=J7OOmMWmTY;noQp9V)vLcs-8f=)9Q>ZnlkNAciVV-X ztpKkn>2?zB!4;WeGax8>ITa!Ko3Sy^Vt6ISzI3~;r;Lggao0-iyb5UW6S#}O|7F(3kq{P~zh4-y1Jq;3gbC498v{kJtrA3{HZmkfL9}1^>6BJHU zq;c~Bq-kvJ-l{rq940Sq1lZi@;r;9tet9z03orKTZuplhaNEm7@0q`e$V)qIv`Y{$ z#EqHBnlndq50NjUikNG-YfCakzqUDwFOW8Ka+b3m_&2N?FuTe^isqvM_^b4S?!Ph~ z+*9=?KJoEBTx$iLAvmi?T8Um(jEl=OxtpAvI2g7MW_EQ?~FWy%c zFV-lQn4`G7zE%^OKK*{PJMPfdY*17%EChSE{gDG@WVVJ)R(aRY7<3Yt7AI?8|4fhT zuf1k0MjjNdDu^`=PK+x`;ggmjmnJD}Iue5fh~5Ggs1i-PyFNpoN6CX~(Vcz5xOvd# z)SOf+c9t(xFhKHDcl$bDiDP)ZtmnOJgQCFqi0Z#H{rO2RzYlEW=52lJSkxYSH49qb z$K1@E&fl(1dWR$pb!z#6XUk3;0nhS@lF0fF`S2uf&>EIy#Hb>A#U87&aoHs{Zq>?Q zCc(5*7Vc~HOCO@-##J2%ij)1p_SUhp`GjD6TtjN6ds#&CSRy>;==3)}u0OlUxYam4 zbNX}9&%GyrnfP}XwP(N*Cd2-Onat=$?@TZyNt7Sw{Xn5un|0q6rS*p>wF$NH=kakh zt-V^J=rUi9b|RsU5EOR@)Yg1yp131>CcGRqJi3WF=VWtVIFzS;O|4WH2p*}kw|r{a zY9wdqO2>SkgR&cvsB!Q(oC<5e7a~&a6Z6|l-y~tdw?ueA*fXH1R?)VZ*>#M-bcK4?FzSa=xK-9VEstGTy0|Y#A zZ$t*dVun8mY%F<^nNtyO{Uq^Y0wrRJ_nuTOape^nhDiUcFi`sqS@J_|OLGIUF&QQw zlLoipcZfxJ8QCx>7vZR~>U_OszORrUmuT3e0h@yMwR$seZeMIQLYwHwx#Yx!;>BD0 zMLpVV(rZiy@6`Vi`MSJAZa_;(?|pbt_XSHLh)7=gnHfN zk`%P-j&}~-%HDH|bk00@U*EW!)~+CLNu~)oMOWo6=t)=JTEXFa{Q;pcQ`0P*=hbvJ8cq$9HY*3%DehK*;WKf&AdHw!~*>*cI6$gS{8yCyA|!&R%3Fmn)0Zbkj@=b z4cUyfUU4i=iRt2`V;1mPIG*a1GEWra!W7TouA&1&5eOL8GN#&W~`dr%c2PR zr7Vu8j=n_g!2$QHLyUC{Q7BsO6IVj%r&ag8|)XuIL z_gCzmE*Ge^l`y<@O;lp?BAD(VzNzUMlp+AF@y=bX-dqj3NXbjj!Nznv2NwDaaMa&= z8wLu)5@;ib2{&PJ{1^r{{ctDSTo-hRjO^jy#pxXw8u&GSMu=4ztFG6e;r;A(FY1gA z_T@uQZ(k6kmakKwsuD$PjlKRJuC>$iW6{NK9**uD2&0YYtKy5lzO3Aj?lsYHw(k^$ z1K|zJi~BzRge!1IzO2U^weB=%H}zW#XQ5sgHCgfhwA)>k8=p8Lh--V8nJY`{Mfttu znPwE|Z?iSu`eNkfz_Ly16gYkqA0SlB8o)BQ7*{EPu1~%6_6kW`2)A)xEO>eJj0S75 z%G?(aE)&369NoS))9S|YH2>(8g`I`y40CuizrDa^`$Y4t0Vz0<`kUbUSo7oner!HY zJf)IaY~g6Ka>$_D z9^4LVFrIf%QdRw%?|k*iO3vgS1=%i#SEXdRgTqgfaWI!d5 zJZcvpn-96K&OFILXY+SOQ!h|8EP+F28K{t|Zwg~F2#$Eqx^CBxdvP74rg@kvuk#W|&xd;Mn9~@!-_n}JE9=VTSdUokg%9u!bn!3~=_vbJ$S%Ul>fXO0A zw$}p(#RD#R9-BTst) zwd5iskC=cV{;W}wlW4N_rbfEnsK>ae!MZv-<_~l3++C zD6osB0!5yS4IuaWZWni8;Edj1q;<#KG9W!NR^4k=Gif6BNXGMd_d!-1jwlo^e9GD# zb@B}K^&I)mUs2RaBt&!TIxqvyv&;oE&B^X+TepgUk8E#AsmOM!`bW}>q)eW}l0o-T z2`XG3fn*Mw&pV-5Ds@Qq!zp&)G!I9daE(Y1_l;CiW^wq#9OA-OZ6N^{C^4Y=B*=O5 zzk>lDQ+qJg`$I$TuJpdl-kE2og6yI<>__>`fl_b-K;5Sb(79ZT56H~en97p!9O#Z2 zl*Rv%9FkgW$tyj{d_Z#o)>Q+3Oz6EXUqN>ewB~2>J<~GMo4)mMT1-EmM^w3V9kD}R zx57@a4h>{CT$$4M1$CWJ1*B-6c~KNJgCeM0eH`*C2wE_oi<01=Zg#v&LV^@?$hZU< zI6P+5MDuhfEe}&}M$$H@F#8o6-kVz?I`eU%qV%CMu=WeIz`eptdQ%v3Fz*E8d(dt?GtN1R2MWlc$?hWTl3h1E&Dw` zM6*HZCL>rwSkjY$nr8bdH<=c}6CkHg(&nn=Qr8Pg4SY1noOH=FIy;W}DB2@j1ce^J zzqfyM&h+Y=EBJvA*>t;~fK;EW2o~$v9Sq3HxoOGfb-i|1sMcIzx83{0;sL?+n3G+g zok-8OrK7p11Y1b?&!pY}E2_v0!MH7N!&+MTm#^@Nm<@v1=`ZzDg{ialr6K`-Q}P%3 zMb&}Y<7C(rO_kHC{fW-zSX|R}eQJQt}|?guR4baXu%=4%mVAAIj@E_LE~PqZV60FOLJXO-bWXe>5!` zvtsi1&q19#addbD_RK52GPlk8c&~Pt@aJjxmE@92@0ltGiDQyWc4bN!jvQ`R3M=p$ zq*IPC-2hu@saz4HyZyy8BjHIfG{Eb&#pkxUVeXjGmuM@<#ji3jE@sU zgoI|cBXmwUiecrGfN$DP8UT0DTd~CIX0-GqylM1NGcL{;s zaLmQxoCO`ByDEDxIltZFJJxv&)aG3V;A#Q4*4)`U(}b@>1mB_Tn=evs$N8hh$(a{! zbWKJ5a){c~_rX*DH{aLhvsV3oyG^{=kTe)NkFt-yxjhFh8M^V8VJZB1CZWC=tiz&BWv?Z6s0=vxwH2KokJZTcx!$$tf?bDo%Ku~j+%(cKw?R~Jm6fth1S$ILZox&R9@p5 z1^j+&1>1I}`p4HaRg*`q@*($DuhOybD7#mU(%SC!EOYu>hR+^AoCUT;5?W@fZ8S}@ z4v)gPDmH{?;6Rn(yvbaNC?^VP&e*_r2|IFB6c_@wyy^# zpEVXcoQkrXdky>ZzCSCl8t`tr<6fOR}L8-(9h&KPbB^Z zBu$hX9E3am9&dV)%WVa6F8BjZz1y__WtsNx#Q(VYC1LI1^~ZLHd+`gePCz3VAIrcj zpy?q|nTxwhjyL^DE{kjv+%0ap+^v{B{+X{7n*?_4Sisz-;B)ZZ6-RBhR z{!|}j=5d;RWpo%x8>&@vy~5NQuF)2Nwwax$5p!!kiU3{{^?z{o^st zQd1lHWDfAZ#@eyrd?BBM0U$>y#{b9+VEG;q=Ht`!T#8?1Cu z6QzGuKFHta!;;-);9|k^d8CTa&Lw)&>Ro>bsB9=qVq;A$od_!6A~Fz1=g%7Ou{PTaA^a1Wp4rmSsy9paZ+Dsn46q zPAv)6SeHK>LiWv|_uK(+&9AAWI!M;Z2i=eQVHI=le@ZqtCvyx|Xw`&*#85n(j;0e&&na zw1}AjmjIIO;hmlk<7_hcj68JH2(*_P*j+th)Y-YPcK&B zODf&&0U{2h(%IB8>JFXf!0wp(%x^4P`;%Usy#v~`E(dyoBwvb*m+j%mooEE^P7E9gYy;3JreDPY8`1X_E(JmuVBJDgz;TIbEGU(+o$bR#isos(x z)#m;iq}4cmd64?9uM8Hi22OXTCaDj$@?K$GW^xb0fN?Y4q}Y$cHv9 z4L(-x&vH3U^>tN;-(9tx=+xK1C&&S5t%$cU3LKG}_OKS;s->fOOF~?J zjH&FOihIwXn*tE*=OzL0b_+~b%7b<{Nq?ki{70WT$-XgU-?`4e+?U|TPVEWnA1tqM zH%OyD2Vp7(=g97J2RuJ@8{o(GGn?+z6o#SxlB#8(7f}Am#e?L0lsD(24~zmsuqypx z1*Trw^KrKUwFAv#+dmpiTh~07PZ;KdJ`XErtMTk@ViY7GAL`7iz$t4_zqo3vM;!2>|- zaH6C_mMpNnj#rqTwx&op>~TbrzJHq+C*)^sXZ#DbF$zlgsaeJ*|4|o*U?sU}e(sKVPq&rW{9YC}Y6&Gr^jW%^YF$yGhK~%reS71Vkd4;uggwfvg@?SoO}-TpY41n<`jMWv@4kJJ($lfWE`rOn|y8^B8s!c z*xL60MDfbMR!9K%_omLdy>Rcd!Cg##!||g;Z(ame!x<`ji!HC!=bz&!#!dQV=XPzZ zy!ZlA*3g;>>}hl%a5XZ*%6H8@d)2W!E-nl8(7ONUHvh7UrCw}tol|ES&Us_9fj*#) zU}VYfDu}mSmHSiq@icX2Z^D&DE9fM4M04!-L&4=3oiX!9#bv7>xPw3FTjvzW`0?KZAiv1p>)ls6A~Yvle){^+*q{j%3N zYcW0XoNkm}Rb+pxr8)tY)-~g#CP!GqdJ)Uus;zliPnlfiW126cRTX99=yndk(jkg` z9f4i>msfLi4TY$U+<_F#G%?oHWWwDceeO)`7eVvX8J%QaqIJvYF*{0lx zwj0~cl(73Y^%9>e+omtvcqM?TL4Kek-&~SMpOf+oYuyq(eQBe#V7Jd110=(aVS=QA z7HBJ>%#{{euads9g8;(DvuTz26YCFmKm-Adw=;fqe)}bT;P8+V-uzI0AX$km%F<>- zLYq~7KQ1x(OQ^2>-`S&ss>ITXt1Mu$h!*3)oPzhD;Wu10afBIc5}pI=zTL)h!xpWU(!Fe) zK~As9sQ{Ll85{&h!LQ&#SYUOdioW!ACm@Y(?QY7@UlnfEX*->6=ZpZ``Zvh;OzvKO4(}k-7b)cpg908pykB32BSmhG zFc;}!;qoS~ACEUjZ6BPx>=s{Vs^bjYm;RSP3LxdU6xH9vUJ1W_)}s+eloUfnlzs@- z>bJN2@r>J0xC{!tro=_56Lml#bhV-4UgcDm+6={*J{HZgzX zm|Og{+~m*8Yp}nmM@Y`k)=%n(1`lB23ve%PUhJde%X2q!sp2Scsf>;WOVTvxWS3aG z_E4GJiGr2pKt!J(1UTt-!WqyBfng7lHb=ypUtlBEH&iziplvSDit$`Zv||gJ*#YOl z6)+n2#+no%ga7kKnTGe&V0tk2Z*Itbmb>O@NnkZ&=&Aw4;UY}>y>@d zj@_sl7Xtm2s&M+9Ca~ts7gM83F{XdTE3XhQ%Ia~gIl}XBnPnG!;4>mYQ+w@bARHTh z$LlbDTW*q%$$UCE?Lyx>IPK1YG}I1j>XXUZ*5le#8sOS_j+>8lQcNw|6(BtKVs8nZ zt|Lw0uZwE~(&Ms7aqF+C9Z|9xN`#M| z&(T1r3EZ&(MN?U2$Lu1UZqS zN_oY(pg}lCfaB?zpyDerzU>&sNb|&F6e!-8)~v0JjJax5g9^P$0&V4TPmo6063FRR zonPF3V^bC6pU~|dA|%_>at#i|R@z+&m*D?zPu0-72_i1=_b)+71^tS0uP>mg7ZYko z+VuJSo>0j4l~Ytd_|T*OIsE87Z9|fE_3*Nyq?Ts|r?nuYhH>8ay-wlKa`+%G>>gu{ zAN$JPl@NyaEXlDz>#_A7!e`U%8vd=?AwwINdHQ72rI`1QNB(tuE@XBFJcn6J#6RBu zeXEZ@?WeX&Pn^0ke!ocBn@yi+Z5a>avhzpEH%^dWUF7Z9qmi8FPXF)_-E-j+SB7$E|)w?X+#SaQU z2gMZ8khAE-RuHwBb$Um@r7^CG0tbUv4-7j4-Jb2XiudROIn;louOl>oW9kN80%Q4YAliKSs$XFI*H z<<^UDU99<_*t>5g^nicmS;N|r1yn<7%4Hq?Cf$LfaUTf(+wdzJX!j|I(PdZH&K;rR z{7ds?)_vdScF6$Jhh7!}cUczovUJs-a@a>_dC8p#8X7w#_5~3@j-YP96>&QTLl)6& z3$~q5weFvd@W=S{*iU^3etrdHDSxayBGw_Gv%KKo@+|I9&Yq7a-2GUB<)7*hOJ#2m z5a$MkH;mU{RNNYj0oy%zN;DPq& z7~eZpYaJ6C(Kmbs6aBZ_3y^eQx=Kq0?7;3Zpbi)tE;BU&)4OWU{=QN zS`~cMh}A3ZzvcT{j%@$OLZOuZyPoXo8?1%qLU$e~{O`1>=e#FyVWKuc^p7VTiV&N7ZNRhkv2txwXdnzj zEVcQkqMR&(Mh?4(*g_5aixZxnfU4~_IR?1a&c^x;V|_rT2=`u-&bAnFOdDq>upoy2 zt@FCWbZXHh5PZ+r*?r~4md!lNX22hzt z<;fKiolpM_ARq$q*W5tsSPy z);WJ*(EU5sPhVQE7;~LSbq`zJ?B2Xp?Yt!b&4N_8rGbf9s@O(tFQZ@{!FV}fwYtMu z7SX4b9v<$mU$a2%^nF+915b%=U6JyXV19@~Ojiy>#?35fk~6phwpw+fqV9X@S1wmy zw;*YETAZMbV?Ie_$X=wVmi?QuD+zP5{Xc=K(%23jOX#aAT<+Kwtz^!Yn*UUfSo*Ir zsMlNsHMDfub(^%EykBv$2Kp7al4UTBnFb|JfO!Rg8K&LLV0g_cx)jF!x_DY??&}r0 z9_n(){Com*NzVVY<&E{&hLrkhs=M`oI zD3hTqi~c)6#SY|+s&?c&*M>f@6O_eXkc>F7Gyic)B^T8ej3#|Wf9-ER;cW*&v#cN_ z0hDaNe}@{~#+(}hTB0_l%$c3shW;0NwK?$*+WuN(x#g=fS7ANr)stc;p!yFkY{_(b zIjEi{mM(L+G}J5JT~!L`Wf9M}Mje>6hRu=KKo6)BB$j?Fj?G!ys11HfM0r_(9B?j+ zts_K{hyQ*$%-^4%#UTOA*pDD2o~r`YV16T_*v1`M_Ermvh>@C<+Z zH*My>v2O^G`#D+9p*OGIUGX{%L2Q_K#Px0y@^5GxYD<(;0M`LIi+!KD$`-GzPL}JM zIOa|)b^Q+b)TgSJFVs4#FN#03qVYG!qImA%7HG@d|LFf}^%=J;oY}4$Hf2%cwQ5t! zp!;Q##l9aoDHO9YWbkV3k*uR#iRSS!5BQq|efzozM7XU>en@{g= zL)<|pb@s#2r(-~dp=_}&;xaA=b(utBxJ6c9B*$vV_JZsQC>S%IUkr%c1~En`L*%Ko zTblU&nEJ=jc4XB3EH!3x1BHS?)}&lMyn3Gb$_T1pvB?AFsHHXt1Srt#^u*J+D&DGF zQ|yrVCg7<92a|*bux%yvCM*-o5)auaNOB}!k1VdkaIB;QA9~%jH z8u0*vn##(EBwq`py7TGYzuMByIbZvfCBc4?DqxCg+fn#Mq*J(Sl7VKwSX#{wT|iJs zDJD@QZ?qYXR?z$)zxJ|eqs?oS;nV+rG=TE?eZ^-<+UejoS}$u<3J8zcIe-*)2E!5!6z7?AR22*(U~1UdNBkP)lLcT7efESXOIDKWO)Z2%F5F@W{{zrh?XeccBk zg2G*J-!s@jnK@9x2h1#UxKRakP(Wulgdnig+~&pp%d-#7kQIFYt#Ed#cwaxF554{0 zpTVihwqqctlEGj{u4Tsiqe^q{n}m=b{}s2<6Qq}v|G^Wxpn(A4F&(zV&Aar7nWnRB z3Z~Uf)weSS8HiUv^)VS9;$5oS2lRQO94LSW0R5oFo4Fg5a{`~wKMeBP@t!?s(15IG zf;gTS)o)C$={kbdQeXr#LEj~JuU`RJwLv2w?@i#t!bf+nD-*qS{@=g%{{Q~Q|L<>c zpu$jw(}i(~wqH)y&-{7-C(Et_+(;pkuGFvajH~kkC`JHT9REP$XZ_!yzOHDn4}<|h zb*9D5$bTkyD}gx)at9a%I0OhPqgD^7CoFTp6h>(_uFlBLKh7ggX5w#~6bUHarvD)@ z*Z!y zQ#^-02)x@>Ve|QBQatBfzBBUe`t7H_Y$oM4>kt#ZGeTz>IvxMH|JlF_iq;WFJ4_yT zXm>=K-#%zN`)R?BpyhIbhj<15(GllDk@RvNL z7aaOD_7D_-eA#=ma;MxH*_t}Npl=-@>gwQ~b!WXYD&=dk-TB5#%Lwzl?#+YS*t^Ik)|WW`TQi3?&Vd@I)F zgjF7aoEiA4Y0R0XtgJQeu-S;lW-n`uY{`?qfBLRR>lSm5apb6TeT%;TL$erLrO|W> zK>ZE!fZU=TDOJ(z|XCa2b*)*YC6RbIF<0KPiqo^3>-N3 zbG}p(Th+E|-xQ%w4RJ>_RB6+4xJ$v~?jA!e+9BH=&u76R&MtCuZTMqkT6G@!Q<>N? znsU(v*kQ@OPU-qIbpHqYobX&9^(}!sd`0oLpaB?L%~I=B^kqZ!6bgEob*Mt9iuh{< zyl&i*pAFtsAAS<@y#sDhW>bV9GB+d=D6LORt?;a-mL2y#FaXbn+oJXz`O6 znU0-~%Je!{`wql_$+Z$4AdqUZ&RPJWUkcD^eN6~VEhgn9<|9&}O8>#4a z2WSn08WEb+hW$!%{F#xL6U6f=m%&b;fWj^ z>|$0|vH#T*L5ND|dk}#>2{}WR8EFz^udfLe7~{@;|8Y&DrDzf9Kf-vA3(G)Ov~$LP zb&Cc1hkwYEEhXf^d(H1F;X#5p;lKVL*+v|2G9i~%OP>7?wPDIgCUD)vf=7wV#m_!; z`)ygYGTnDv^43Z6rJPr1VXPn z35C*+ok#|ZwXW6rU6RcnTsRUTG1nIbHp3r0^3&)jOOl0%*49gh>VA{@x{J!;F8HHe zQS8viL*%xvYXpl_Q4jDMIj-`TV~F0ulGdDDKy6f}@ukA@8yu@V8G9#Zb1QSjAn=cD MXij>4%;nnu11~c@Pyhe` literal 0 HcmV?d00001 diff --git a/docs/assets/ds-symbol-positive-mono.png b/docs/assets/ds-symbol-positive-mono.png new file mode 100644 index 0000000000000000000000000000000000000000..c2ed5e2fc70f2f47b854631db68ddd693f49ccfe GIT binary patch literal 36816 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 + + + diff --git a/docs/tips.md b/docs/tips.md new file mode 100644 index 00000000..3727f6ca --- /dev/null +++ b/docs/tips.md @@ -0,0 +1,28 @@ +# Tips + +## Root Paths + +The proxy can be optionally served from a non-root path (e.g., `/api/v1`). Additionally, the proxy can optionally proxy requests to an upstream API served from a non-root path (e.g., `/stac`). To handle this, the proxy will: + +- Remove the `ROOT_PATH` from incoming requests before forwarding to the upstream API +- Remove the proxy's prefix from all links in STAC API responses +- Add the `ROOT_PATH` prefix to all links in STAC API responses +- Update the OpenAPI specification to include the `ROOT_PATH` in the servers field +- Handle requests that don't match the `ROOT_PATH` with a 404 response + +## Non-OIDC Workaround + +If the upstream server utilizes RS256 JWTs but does not utilize a proper OIDC server, the proxy can be configured to work around this by setting the `OIDC_DISCOVERY_URL` to a statically-hosted OIDC discovery document that points to a valid JWKS endpoint. + +## Swagger UI Direct JWT Input + +Rather than performing the login flow, the Swagger UI can be configured to accept direct JWT as input with the the following configuration: + +```sh +OPENAPI_AUTH_SCHEME_NAME=jwtAuth +OPENAPI_AUTH_SCHEME_OVERRIDE={"type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "Paste your raw JWT here. This API uses Bearer token authorization."} +``` + +## Runtime Customization + +While the project is designed to work out-of-the-box as an application, it might not address every projects needs. When the need for customization arises, the codebase can instead be treated as a library of components that can be used to augment any [ASGI](https://asgi.readthedocs.io/en/latest/)-compliant webserver (e.g. [Django](https://docs.djangoproject.com/en/3.0/topics/async/), [Falcon](https://falconframework.org/), [FastAPI](https://github.com/tiangolo/fastapi), [Litestar](https://litestar.dev/), [Responder](https://responder.readthedocs.io/en/latest/), [Sanic](https://sanic.dev/), [Starlette](https://www.starlette.io/)). Review [`app.py`](https://github.com/developmentseed/stac-auth-proxy/blob/main/src/stac_auth_proxy/app.py) to get a sense of how we make use of the various components to construct a FastAPI application. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..dc88aa64 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,123 @@ +site_name: STAC Auth Proxy +site_description: A reverse proxy to mediate communication between a client and an internally accessible STAC API in order to provide a flexible authentication mechanism. +site_dir: build + +repo_name: developmentseed/stac-auth-proxy +repo_url: https://github.com/developmentseed/stac-auth-proxy +edit_uri: blob/main/docs/ +site_url: https://developmentseed.org/stac-auth-proxy + +extra: + analytics: + provider: plausible + domain: developmentseed.org/stac-auth-proxy + + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: good + note: Thanks for your feedback! + + - icon: material/emoticon-sad-outline + name: This page could be improved + data: bad + note: Thanks for your feedback! + social: + - icon: fontawesome/brands/github + link: https://github.com/developmentseed +nav: + - Overview: index.md + - Installation and Running: installation-and-running.md + - Configuration: configuration.md + - Architecture: + - Middleware Stack: architecture/middleware-stack.md + - Data Filtering: architecture/data-filtering.md + - Tips: tips.md + +plugins: + - search + - social + - api-autonav: + modules: + - src/stac_auth_proxy + - mkdocstrings: + enable_inventory: true + handlers: + python: + paths: + - src + options: + extensions: + - griffe_fieldz + show_signature_annotations: true + inventories: + - https://docs.python.org/3/objects.inv + - https://docs.pydantic.dev/latest/objects.inv + - https://fastapi.tiangolo.com/objects.inv + - https://www.starlette.io/objects.inv + +theme: + name: material + palette: + # Palette toggle for automatic mode + - media: (prefers-color-scheme) + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + + custom_dir: docs/overrides + favicon: assets/ds-symbol-positive-mono.png + logo: assets/ds-symbol-negative-mono.png + + features: + - content.code.annotate + - content.code.copy + - navigation.indexes + - navigation.instant + - navigation.tracking + - search.suggest + - search.share + +# https://github.com/kylebarron/cogeo-mosaic/blob/mkdocs/mkdocs.yml#L50-L75 +markdown_extensions: + - attr_list + - codehilite: + guess_lang: false + - def_list + - footnotes + - markdown_gfm_admonition # support github-flavored admonitions + - pymdownx.emoji: # Convert emoji shortcodes to images + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.magiclink: # render hrefs as links + hide_protocol: true + repo_url_shortener: true # handle github links + - pymdownx.superfences: + custom_fences: + - name: mermaid # support mermaid diagrams + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - toc: + permalink: true # Add permalink hover link to each heading + - pymdownx.tasklist: + custom_checkbox: true diff --git a/pyproject.toml b/pyproject.toml index 5bc05565..390f5a1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,17 @@ readme = "README.md" requires-python = ">=3.9" version = "0.7.1" +[project.optional-dependencies] +docs = [ + "griffe-fieldz>=0.3.0", + "griffe-inherited-docstrings>=1.1.1", + "markdown-gfm-admonition>=0.1.1", + "mkdocs>=1.6.1", + "mkdocs-api-autonav>=0.3.0", + "mkdocs-material[imaging]>=9.6.16", + "mkdocstrings[python]>=0.30.0", +] + [tool.coverage.run] branch = true diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index 1648282b..c1d8bedc 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -16,7 +16,7 @@ _PREFIX_PATTERN = r"^/.*$" -class ClassInput(BaseModel): +class _ClassInput(BaseModel): """Input model for dynamically loading a class or function.""" cls: str @@ -76,9 +76,9 @@ class Settings(BaseSettings): } # Filters - items_filter: Optional[ClassInput] = None + items_filter: Optional[_ClassInput] = None items_filter_path: str = r"^(/collections/([^/]+)/items(/[^/]+)?$|/search$)" - collections_filter: Optional[ClassInput] = None + collections_filter: Optional[_ClassInput] = None collections_filter_path: str = r"^/collections(/[^/]+)?$" model_config = SettingsConfigDict( @@ -87,7 +87,7 @@ class Settings(BaseSettings): @model_validator(mode="before") @classmethod - def default_oidc_discovery_internal_url(cls, data: Any) -> Any: + def _default_oidc_discovery_internal_url(cls, data: Any) -> Any: """Set the internal OIDC discovery URL to the public URL if not set.""" if not data.get("oidc_discovery_internal_url"): data["oidc_discovery_internal_url"] = data.get("oidc_discovery_url") diff --git a/uv.lock b/uv.lock index 354cbaf4..f1a5dbae 100644 --- a/uv.lock +++ b/uv.lock @@ -30,6 +30,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041, upload-time = "2025-01-05T13:13:07.985Z" }, ] +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, +] + [[package]] name = "boto3" version = "1.37.16" @@ -144,6 +167,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/b3/f7b3af539f74b82e1c64d28685a5200c631cc14ae751d37d6ed819655627/Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", size = 357258, upload-time = "2023-09-07T14:05:39.591Z" }, ] +[[package]] +name = "cairocffi" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" }, +] + +[[package]] +name = "cairosvg" +version = "2.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cairocffi" }, + { name = "cssselect2" }, + { name = "defusedxml" }, + { name = "pillow" }, + { name = "tinycss2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/5106168bd43d7cd8b7cc2a2ee465b385f14b63f4c092bb89eee2d48c8e67/cairosvg-2.8.2.tar.gz", hash = "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", size = 8398590, upload-time = "2025-05-15T06:56:32.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/48/816bd4aaae93dbf9e408c58598bc32f4a8c65f4b86ab560864cb3ee60adb/cairosvg-2.8.2-py3-none-any.whl", hash = "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5", size = 45773, upload-time = "2025-05-15T06:56:28.552Z" }, +] + [[package]] name = "certifi" version = "2025.1.31" @@ -231,6 +282,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, + { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, + { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, + { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + [[package]] name = "click" version = "8.1.8" @@ -561,6 +686,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2e/38/3fd83c4690dc7d753a442a284b3826ea5e5c380a411443c66421cd823898/cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7", size = 3134657, upload-time = "2025-02-11T15:50:47.6Z" }, ] +[[package]] +name = "cssselect2" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tinycss2" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "distlib" version = "0.3.9" @@ -602,6 +749,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/7d/2d6ce181d7a5f51dedb8c06206cbf0ec026a99bf145edd309f9e17c3282f/fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf", size = 94814, upload-time = "2025-01-30T14:06:38.564Z" }, ] +[[package]] +name = "fieldz" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/62/698c5cc2e7d4c8c89e63033e2e9d3c74902a1bf28782712eacb0653097ce/fieldz-0.1.2.tar.gz", hash = "sha256:0448ed5dacb13eaa49da0db786e87fae298fbd2652d26c510e5d7aea6b6bebf4", size = 17277, upload-time = "2025-06-30T18:06:40.881Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/8c/8958392cade27a272daf45d09a08473073dedeccad94b097dfeb898d969f/fieldz-0.1.2-py3-none-any.whl", hash = "sha256:e25884d2821a2d5638ef8d4d8bce5d1039359cfcb46d0f93df8cb1f7c2eb3a2e", size = 17878, upload-time = "2025-06-30T18:06:39.322Z" }, +] + [[package]] name = "filelock" version = "3.17.0" @@ -611,6 +770,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164, upload-time = "2025-01-21T20:04:47.734Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/42/486d21a6c33ff69a7381511d507b6db7a7b7f4d5bec3279bc0dc45c658a9/griffe-1.9.0.tar.gz", hash = "sha256:b5531cf45e9b73f0842c2121cc4d4bcbb98a55475e191fc9830e7aef87a920a0", size = 409341, upload-time = "2025-07-28T17:45:38.712Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/65/7b3fcef8c9fb6d1023484d9caf87e78450a5c9cd1e191ce9632990b65284/griffe-1.9.0-py3-none-any.whl", hash = "sha256:bcf90ee3ad42bbae70a2a490c782fc8e443de9b84aa089d857c278a4e23215fc", size = 137060, upload-time = "2025-07-28T17:45:36.973Z" }, +] + +[[package]] +name = "griffe-fieldz" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fieldz" }, + { name = "griffe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/6a/94754bf39fd63ba424c667b2abf0ade78e3878e223591d1fb9c3e8a77bce/griffe_fieldz-0.3.0.tar.gz", hash = "sha256:42e7707dac51d38e26fb7f3f7f51429da9b47e98060bfeb81a4287456d5b8a89", size = 10149, upload-time = "2025-07-30T21:43:10.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/33/cc527c11132a6274724a04938d50e1ff2b54a5f5943cd0480427571e1adb/griffe_fieldz-0.3.0-py3-none-any.whl", hash = "sha256:52e02fdcbdf6dea3c8c95756d1e0b30861569f871d19437fda702776fde4e64d", size = 6577, upload-time = "2025-07-30T21:43:09.073Z" }, +] + +[[package]] +name = "griffe-inherited-docstrings" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/9f/098599019b2715e1edad3618305b8acf253e2ee375cbd389507cb23a2a00/griffe_inherited_docstrings-1.1.1.tar.gz", hash = "sha256:d179b6a6b7dc260fb892ad5b857837afd6f9de6193fc26d14463c4e9975a0cd3", size = 24146, upload-time = "2024-11-05T13:46:05.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f9/51a3fd7460b95583ff470c7b4fd706bd21f3fda97d521f3770126dc6d1fc/griffe_inherited_docstrings-1.1.1-py3-none-any.whl", hash = "sha256:0cb613ade70793b3589c706269a2cc4ceb91cbc4cfdc651037839cb9506eabe6", size = 6008, upload-time = "2024-11-05T13:46:03.504Z" }, +] + [[package]] name = "h11" version = "0.14.0" @@ -702,6 +910,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -745,6 +965,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cd/58/4a1880ea64032185e9ae9f63940c9327c6952d5584ea544a8f66972f2fda/jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", size = 92520, upload-time = "2024-03-06T19:58:29.765Z" }, ] +[[package]] +name = "markdown" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, +] + +[[package]] +name = "markdown-gfm-admonition" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/80/05c5b58356c85c336e88eaaae49bd058917531504914dd1c95b5a52a851b/markdown_gfm_admonition-0.1.1.tar.gz", hash = "sha256:a0cb00d183b2be4a4bfc355a74500ddb9818c7ce8d9ac99a63ba7f689866c13d", size = 6384, upload-time = "2024-08-01T10:42:41.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/fe/e7ccdd9e542afebf61e95f0a0ce0557c494e798d8ca39903a7f9bd83bdca/markdown_gfm_admonition-0.1.1-py3-none-any.whl", hash = "sha256:f6626287a34adf0cdc44f953086fcb9aaaf0cb698f01f3a36b46758c30f7d701", size = 8740, upload-time = "2024-08-01T10:42:40.035Z" }, +] + [[package]] name = "markupsafe" version = "3.0.2" @@ -813,6 +1057,158 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-api-autonav" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, + { name = "mkdocstrings-python" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/39/4f24167d977a70eb72afcea7632fd0ecca3dd0e63081d8060c0ea050aeef/mkdocs_api_autonav-0.3.0.tar.gz", hash = "sha256:1c0f10c69db38bd35d9c343814c50c033224b790e68b45876ca7e3cdfd25005c", size = 74239, upload-time = "2025-06-13T14:58:38.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/5c/19f8b99d248f3fc99283374d4eadbc1863439b0a6f31eb396a650a9ad315/mkdocs_api_autonav-0.3.0-py3-none-any.whl", hash = "sha256:3e5fce7a43e1a131b31e23b2391cde8b189a0a0aa772b74782c7141c3617e618", size = 12169, upload-time = "2025-06-13T14:58:36.972Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.6.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/84/aec27a468c5e8c27689c71b516fb5a0d10b8fca45b9ad2dd9d6e43bc4296/mkdocs_material-9.6.16.tar.gz", hash = "sha256:d07011df4a5c02ee0877496d9f1bfc986cfb93d964799b032dd99fe34c0e9d19", size = 4028828, upload-time = "2025-07-26T15:53:47.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f4/90ad67125b4dd66e7884e4dbdfab82e3679eb92b751116f8bb25ccfe2f0c/mkdocs_material-9.6.16-py3-none-any.whl", hash = "sha256:8d1a1282b892fe1fdf77bfeb08c485ba3909dd743c9ba69a19a40f637c6ec18c", size = 9223743, upload-time = "2025-07-26T15:53:44.236Z" }, +] + +[package.optional-dependencies] +imaging = [ + { name = "cairosvg" }, + { name = "pillow" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -831,6 +1227,101 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pillow" +version = "10.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload-time = "2024-07-01T09:48:43.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271, upload-time = "2024-07-01T09:45:22.07Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658, upload-time = "2024-07-01T09:45:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075, upload-time = "2024-07-01T09:45:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808, upload-time = "2024-07-01T09:45:30.305Z" }, + { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290, upload-time = "2024-07-01T09:45:32.868Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163, upload-time = "2024-07-01T09:45:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100, upload-time = "2024-07-01T09:45:37.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880, upload-time = "2024-07-01T09:45:39.89Z" }, + { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218, upload-time = "2024-07-01T09:45:42.771Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487, upload-time = "2024-07-01T09:45:45.176Z" }, + { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219, upload-time = "2024-07-01T09:45:47.274Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265, upload-time = "2024-07-01T09:45:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655, upload-time = "2024-07-01T09:45:52.462Z" }, + { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304, upload-time = "2024-07-01T09:45:55.006Z" }, + { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804, upload-time = "2024-07-01T09:45:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126, upload-time = "2024-07-01T09:46:00.713Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541, upload-time = "2024-07-01T09:46:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616, upload-time = "2024-07-01T09:46:05.356Z" }, + { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802, upload-time = "2024-07-01T09:46:08.145Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213, upload-time = "2024-07-01T09:46:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498, upload-time = "2024-07-01T09:46:12.685Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219, upload-time = "2024-07-01T09:46:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350, upload-time = "2024-07-01T09:46:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980, upload-time = "2024-07-01T09:46:19.169Z" }, + { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799, upload-time = "2024-07-01T09:46:21.883Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973, upload-time = "2024-07-01T09:46:24.321Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054, upload-time = "2024-07-01T09:46:26.825Z" }, + { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484, upload-time = "2024-07-01T09:46:29.355Z" }, + { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375, upload-time = "2024-07-01T09:46:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773, upload-time = "2024-07-01T09:46:33.73Z" }, + { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690, upload-time = "2024-07-01T09:46:36.587Z" }, + { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951, upload-time = "2024-07-01T09:46:38.777Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427, upload-time = "2024-07-01T09:46:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685, upload-time = "2024-07-01T09:46:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883, upload-time = "2024-07-01T09:46:47.331Z" }, + { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837, upload-time = "2024-07-01T09:46:49.647Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562, upload-time = "2024-07-01T09:46:51.811Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761, upload-time = "2024-07-01T09:46:53.961Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767, upload-time = "2024-07-01T09:46:56.664Z" }, + { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989, upload-time = "2024-07-01T09:46:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255, upload-time = "2024-07-01T09:47:01.189Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603, upload-time = "2024-07-01T09:47:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972, upload-time = "2024-07-01T09:47:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375, upload-time = "2024-07-01T09:47:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/31/85/955fa5400fa8039921f630372cfe5056eed6e1b8e0430ee4507d7de48832/pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", size = 3509283, upload-time = "2024-07-01T09:47:36.394Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/343827267eb28d41cd82b4180d33b10d868af9077abcec0af9793aa77d2d/pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", size = 3375691, upload-time = "2024-07-01T09:47:38.853Z" }, + { url = "https://files.pythonhosted.org/packages/60/a3/7ebbeabcd341eab722896d1a5b59a3df98c4b4d26cf4b0385f8aa94296f7/pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", size = 4328295, upload-time = "2024-07-01T09:47:41.765Z" }, + { url = "https://files.pythonhosted.org/packages/32/3f/c02268d0c6fb6b3958bdda673c17b315c821d97df29ae6969f20fb49388a/pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", size = 4440810, upload-time = "2024-07-01T09:47:44.27Z" }, + { url = "https://files.pythonhosted.org/packages/67/5d/1c93c8cc35f2fdd3d6cc7e4ad72d203902859a2867de6ad957d9b708eb8d/pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", size = 4352283, upload-time = "2024-07-01T09:47:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a8/8655557c9c7202b8abbd001f61ff36711cefaf750debcaa1c24d154ef602/pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", size = 4521800, upload-time = "2024-07-01T09:47:48.813Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/6f95797af64d137124f68af1bdaa13b5332da282b86031f6fa70cf368261/pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", size = 4459177, upload-time = "2024-07-01T09:47:52.104Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/2b3ce34f1c4266d79a78c9a51d1289a33c3c02833fe294ef0dcbb9cba4ed/pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", size = 4589079, upload-time = "2024-07-01T09:47:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/456258c74da1ff5bf8ef1eab06a95ca994d8b9ed44c01d45c3f8cbd1db7e/pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", size = 2235247, upload-time = "2024-07-01T09:47:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/bef952bdb32aa53741f58bf21798642209e994edc3f6598f337f23d5400a/pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", size = 2554479, upload-time = "2024-07-01T09:47:59.881Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8e/805201619cad6651eef5fc1fdef913804baf00053461522fabbc5588ea12/pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", size = 2243226, upload-time = "2024-07-01T09:48:02.508Z" }, + { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889, upload-time = "2024-07-01T09:48:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160, upload-time = "2024-07-01T09:48:07.206Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020, upload-time = "2024-07-01T09:48:09.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539, upload-time = "2024-07-01T09:48:12.529Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125, upload-time = "2024-07-01T09:48:14.891Z" }, + { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373, upload-time = "2024-07-01T09:48:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661, upload-time = "2024-07-01T09:48:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1f/5a9fcd6ced51633c22481417e11b1b47d723f64fb536dfd67c015eb7f0ab/pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", size = 3493850, upload-time = "2024-07-01T09:48:23.03Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e6/3ea4755ed5320cb62aa6be2f6de47b058c6550f752dd050e86f694c59798/pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", size = 3346118, upload-time = "2024-07-01T09:48:25.256Z" }, + { url = "https://files.pythonhosted.org/packages/0a/22/492f9f61e4648422b6ca39268ec8139277a5b34648d28f400faac14e0f48/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", size = 3434958, upload-time = "2024-07-01T09:48:28.078Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/559a48ad4045704bb0547965b9a9345f5cd461347d977a56d178db28819e/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", size = 3490340, upload-time = "2024-07-01T09:48:30.734Z" }, + { url = "https://files.pythonhosted.org/packages/d9/de/cebaca6fb79905b3a1aa0281d238769df3fb2ede34fd7c0caa286575915a/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", size = 3476048, upload-time = "2024-07-01T09:48:33.292Z" }, + { url = "https://files.pythonhosted.org/packages/71/f0/86d5b2f04693b0116a01d75302b0a307800a90d6c351a8aa4f8ae76cd499/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", size = 3579366, upload-time = "2024-07-01T09:48:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/37/ae/2dbfc38cc4fd14aceea14bc440d5151b21f64c4c3ba3f6f4191610b7ee5d/pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", size = 2554652, upload-time = "2024-07-01T09:48:38.789Z" }, +] + [[package]] name = "platformdirs" version = "4.3.6" @@ -998,6 +1489,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/a9/3b9642025174bbe67e900785fb99c9bfe91ea584b0b7126ff99945c24a0e/pydantic_settings-2.8.0-py3-none-any.whl", hash = "sha256:c782c7dc3fb40e97b238e713c25d26f64314aece2e91abcff592fcac15f71820", size = 30746, upload-time = "2025-02-21T08:04:50.49Z" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + [[package]] name = "pyjwt" version = "2.10.1" @@ -1007,6 +1507,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, +] + [[package]] name = "pytest" version = "8.3.4" @@ -1136,6 +1649,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + [[package]] name = "s3transfer" version = "0.11.4" @@ -1168,7 +1708,7 @@ wheels = [ [[package]] name = "stac-auth-proxy" -version = "0.6.0" +version = "0.7.1" source = { editable = "." } dependencies = [ { name = "boto3" }, @@ -1184,6 +1724,17 @@ dependencies = [ { name = "uvicorn" }, ] +[package.optional-dependencies] +docs = [ + { name = "griffe-fieldz" }, + { name = "griffe-inherited-docstrings" }, + { name = "markdown-gfm-admonition" }, + { name = "mkdocs" }, + { name = "mkdocs-api-autonav" }, + { name = "mkdocs-material", extra = ["imaging"] }, + { name = "mkdocstrings", extra = ["python"] }, +] + [package.dev-dependencies] dev = [ { name = "jwcrypto" }, @@ -1202,13 +1753,21 @@ requires-dist = [ { name = "cql2", specifier = ">=0.3.6" }, { name = "cryptography", specifier = ">=44.0.1" }, { name = "fastapi", specifier = ">=0.115.5" }, + { name = "griffe-fieldz", marker = "extra == 'docs'", specifier = ">=0.3.0" }, + { name = "griffe-inherited-docstrings", marker = "extra == 'docs'", specifier = ">=1.1.1" }, { name = "httpx", extras = ["http2"], specifier = ">=0.28.0" }, { name = "jinja2", specifier = ">=3.1.4" }, + { name = "markdown-gfm-admonition", marker = "extra == 'docs'", specifier = ">=0.1.1" }, + { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6.1" }, + { name = "mkdocs-api-autonav", marker = "extra == 'docs'", specifier = ">=0.3.0" }, + { name = "mkdocs-material", extras = ["imaging"], marker = "extra == 'docs'", specifier = ">=9.6.16" }, + { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.30.0" }, { name = "pydantic-settings", specifier = ">=2.6.1" }, { name = "pyjwt", specifier = ">=2.10.1" }, { name = "starlette-cramjam", specifier = ">=0.4.0" }, { name = "uvicorn", specifier = ">=0.32.1" }, ] +provides-extras = ["docs"] [package.metadata.requires-dev] dev = [ @@ -1247,6 +1806,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/46/de94ca00de3505c2d7b802ac5e581eef1fb0b3039966d7655aa7b53f6821/starlette_cramjam-0.4.0-py3-none-any.whl", hash = "sha256:c1943087641c8ed5a08fc166664875a1f44c6f1de4301ed21f23a261df821c1b", size = 7201, upload-time = "2024-10-17T16:10:51.203Z" }, ] +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + [[package]] name = "tomli" version = "2.2.1" @@ -1331,3 +1902,58 @@ sdist = { url = "https://files.pythonhosted.org/packages/f1/88/dacc875dd54a8acad wheels = [ { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478, upload-time = "2025-02-10T19:03:48.221Z" }, ] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]