From 3523d8bd0f5d9b898cbb7f418198ca1ef3e877d7 Mon Sep 17 00:00:00 2001 From: avantol Date: Wed, 5 Aug 2026 14:45:59 -0500 Subject: [PATCH 1/7] feat(dpop): initial dpop support, no tests yet --- .github/workflows/ci.yaml | 4 +- .pre-commit-config.yaml | 6 +- .secrets.baseline | 58 +- README.md | 10 +- docs/howto/nextflow.md | 276 +++++++ gen3/auth.py | 12 +- gen3/cli/__main__.py | 4 + gen3/cli/dpop.py | 98 +++ gen3/cli/nextflow.py | 122 +++ gen3/dpop.py | 996 ++++++++++++++++++++++++ gen3/dpop_nextflow.py | 188 +++++ poetry.lock | 1018 ++++++------------------- pyproject.toml | 33 +- tests/download_tests/test_download.py | 10 +- 14 files changed, 1990 insertions(+), 845 deletions(-) create mode 100644 docs/howto/nextflow.md create mode 100644 gen3/cli/dpop.py create mode 100644 gen3/cli/nextflow.py create mode 100644 gen3/dpop.py create mode 100644 gen3/dpop_nextflow.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 068d548de..937e907fb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,8 +20,8 @@ jobs: matrix: # the versions for which we require the CI to pass on PRs can # be updated in the repo settings (branch rules) - version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + version: ['3.13'] fail-fast: false # run for all versions even if one of them fails with: python-version: ${{ matrix.version }} - poetry-version: '2.2.1' # Max version supported by python 3.9 + poetry-version: '2.2.1' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 352da81e4..c2bd895e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: git@github.com:Yelp/detect-secrets - rev: v1.4.0 + rev: v1.5.0 hooks: - id: detect-secrets args: ['--baseline', '.secrets.baseline'] exclude: '(docs\/_build|poetry.lock)' - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: no-commit-to-branch args: [--branch, develop, --branch, master, --pattern, release/.*] - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 26.5.1 hooks: - id: black additional_dependencies: ['click==8.0.4'] diff --git a/.secrets.baseline b/.secrets.baseline index 748400d46..3bc771eed 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1,5 +1,5 @@ { - "version": "1.4.0", + "version": "1.5.0", "plugins_used": [ { "name": "ArtifactoryDetector" @@ -26,6 +26,9 @@ { "name": "GitHubTokenDetector" }, + { + "name": "GitLabTokenDetector" + }, { "name": "HexHighEntropyString", "limit": 3.0 @@ -36,6 +39,9 @@ { "name": "IbmCosHmacDetector" }, + { + "name": "IPPublicDetector" + }, { "name": "JwtTokenDetector" }, @@ -49,9 +55,15 @@ { "name": "NpmDetector" }, + { + "name": "OpenAIDetector" + }, { "name": "PrivateKeyDetector" }, + { + "name": "PypiTokenDetector" + }, { "name": "SendGridDetector" }, @@ -67,6 +79,9 @@ { "name": "StripeDetector" }, + { + "name": "TelegramBotTokenDetector" + }, { "name": "TwilioKeyDetector" } @@ -75,10 +90,6 @@ { "path": "detect_secrets.filters.allowlist.is_line_allowlisted" }, - { - "path": "detect_secrets.filters.common.is_baseline_file", - "filename": ".secrets.baseline" - }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", "min_level": 2 @@ -112,33 +123,33 @@ } ], "results": { - ".travis.yml": [ + "docs/_build/html/.buildinfo": [ { - "type": "Base64 High Entropy String", - "filename": ".travis.yml", - "hashed_secret": "2b450d45e8b520a9b25727d563d1d7e036497655", + "type": "Hex High Entropy String", + "filename": "docs/_build/html/.buildinfo", + "hashed_secret": "261dc6f20f0787be0129e7c1442d3393175e68aa", "is_verified": false, - "line_number": 47 + "line_number": 3 }, { - "type": "Base64 High Entropy String", - "filename": ".travis.yml", - "hashed_secret": "8dbd93a5374b178c4c142aa603c6daf3edd8413e", + "type": "Hex High Entropy String", + "filename": "docs/_build/html/.buildinfo", + "hashed_secret": "63256a38625700f409b7ec5db8fa05ffd2cb88b2", "is_verified": false, - "line_number": 48 + "line_number": 4 } ], - "docs/_build/html/.buildinfo": [ + "docs/_build/html/.buildinfo.bak": [ { "type": "Hex High Entropy String", - "filename": "docs/_build/html/.buildinfo", - "hashed_secret": "52acb5a7ffb1bfafa0e4f02cfbbfc70c59e7906d", + "filename": "docs/_build/html/.buildinfo.bak", + "hashed_secret": "af3608e21ad7b453bd6a54b55735a397ed6cb820", "is_verified": false, "line_number": 3 }, { "type": "Hex High Entropy String", - "filename": "docs/_build/html/.buildinfo", + "filename": "docs/_build/html/.buildinfo.bak", "hashed_secret": "63256a38625700f409b7ec5db8fa05ffd2cb88b2", "is_verified": false, "line_number": 4 @@ -192,6 +203,15 @@ "line_number": 75 } ], + "gen3/dpop_nextflow.py": [ + { + "type": "Secret Keyword", + "filename": "gen3/dpop_nextflow.py", + "hashed_secret": "08d2e98e6754af941484848930ccbaddfefe13d6", + "is_verified": false, + "line_number": 54 + } + ], "tests/bundle_tests/test_bundle_ingestion.py": [ { "type": "Hex High Entropy String", @@ -656,5 +676,5 @@ } ] }, - "generated_at": "2025-11-07T21:07:45Z" + "generated_at": "2026-08-05T19:45:34Z" } diff --git a/README.md b/README.md index 4f154b458..097adc84b 100755 --- a/README.md +++ b/README.md @@ -9,18 +9,19 @@ The docs here contain general descriptions of the different pieces of the SDK an * [Detailed API Documentation](https://uc-cdis.github.io/gen3sdk-python/_build/html/index.html) - ## Prerequisites This project is built with Python. Ensure you have Python 3.6 or later installed. Other prerequisites include: + - [pip](https://pip.pypa.io/en/stable/) - Access to a Gen3 commons. ## Installation Steps: ### Using pip + To install the latest released version of the SDK, run: ``` @@ -41,9 +42,10 @@ Additional documentation for different components is available: - [Available Classes](docs/reference/sdkClasses.md) - [Indexing Tools](docs/howto/diirmIndexing.md) - [Metadata Tools](docs/howto/metadataTools.md) - - [Gen3 Discovery Page Metadata Tools](docs/howto/discoveryMetadataTools.md) - - [Gen3 Subject-level Crosswalk Metadata Tools](docs/howto/crosswalk.md) + - [Gen3 Discovery Page Metadata Tools](docs/howto/discoveryMetadataTools.md) + - [Gen3 Subject-level Crosswalk Metadata Tools](docs/howto/crosswalk.md) - [Bundle Tools](docs/howto/bundleTools.md) +- [Nextflow with the Gen3 DPoP Proxy](docs/howto/nextflow.md) - [Development](docs/howto/devTest.md) - [CLI](docs/howto/cli.md) @@ -55,4 +57,4 @@ For FAQs and commonly encountered errors, consult the documentation or use the C gen3 --help ``` -If you encounter issues, raise them on the [Gen3 SDK GitHub Issues page](https://github.com/uc-cdis/gen3sdk-python/issues). +If you encounter issues, raise them on the [Gen3 SDK GitHub Issues page](https://github.com/uc-cdis/gen3sdk-python/issues). \ No newline at end of file diff --git a/docs/howto/nextflow.md b/docs/howto/nextflow.md new file mode 100644 index 000000000..b66641933 --- /dev/null +++ b/docs/howto/nextflow.md @@ -0,0 +1,276 @@ +# Nextflow with the Gen3 DPoP Proxy + +This guide covers running Nextflow pipelines against Gen3 Workflow (GA4GH TES plus the +Gen3 S3 endpoint) with DPoP-bound task tokens. + +## Overview + +`gen3 nextflow run` does three things before it launches Nextflow: + +1. Generates an ephemeral EC P-256 key in memory. +2. Exchanges your Gen3 API key for a **DPoP-bound task token** at + `{commons}/user/credentials/api/access_token`. The token is bound to the public key's + thumbprint, so possessing the token alone is not enough to use it. +3. Starts a local proxy (a raw ASGI app on uvicorn, bound to `127.0.0.1`) that signs + every outgoing request with a fresh DPoP proof and forwards it to the TES or S3 + endpoint. + +Nextflow is then run as a child process with two environment variables set: + +| Variable | Meaning | +| ---------------------------- | ------------------------------------ | +| `GEN3_DPOP_PROXY_PORT` | Port the local proxy is listening on | +| `GEN3_DPOP_BOUND_TASK_TOKEN` | The DPoP-bound task token | + +`GEN3_DPOP_PROXY_PORT` is read on the way in as well as written on the way out: set it +yourself and the proxy binds that port instead of asking the OS for a free one, so the +port stops changing between runs. `--port` overrides it. + +```bash +export GEN3_DPOP_PROXY_PORT=8000 +``` + +Requests reaching the proxy are routed by path prefix: `/ga4gh/tes` goes to the TES +endpoint (`{commons}/ga4gh/tes` by default) and `/s3` goes to the S3 endpoint +(`{commons}/workflows/s3` by default). Every forwarded request gets a fresh `DPoP` proof +header. On the TES route the proxy also replaces whatever `Authorization` header arrived +with `Authorization: DPoP `. On the S3 route it does not: Gen3's S3 endpoint +reads the task token out of the SigV4 `Credential=/...` field of the header +Nextflow signed, so that header is forwarded untouched and the proof travels in `DPoP` +alone. + +Nothing else is proxied. Any other path — and any path that uses `..` to climb out of the +prefix it matched — gets a local `404` and is never sent anywhere. Everything the proxy +does forward carries the task token, so an unrecognized path is refused rather than +guessed at. + +## Prerequisites + +- The SDK with the `dpop` extra: `pip install 'gen3[dpop]'` +- `nextflow` on your `PATH` +- Gen3 credentials — an API key at `~/.gen3/credentials.json` (see + [the CLI guide](howto/cli.md) for the other forms `--auth` accepts) + +## Configuring your pipeline + +Keep the config you already use with Gen3 Workflow — the one that points straight at your +commons. `gen3 nextflow run` does not edit it. It writes a small override to a temporary +file and passes it as the last `-c`, and Nextflow merges `-c` files over the existing +config rather than replacing them, so only the keys the proxy forces change. A normal +`nextflow.config`: + +```groovy +plugins { + id 'nf-ga4gh' +} +process { + executor = 'tes' + container = 'quay.io/nextflow/bash' +} +tes { + endpoint = 'https:///ga4gh/tes' + oauthToken = env('GEN3_TOKEN') + timeout = 60 +} +aws { + accessKey = env('GEN3_TOKEN') + secretKey = 'N/A' # pragma: allowlist secret + region = 'us-east-1' + client { + endpoint = 'https:///workflows/s3' + s3PathStyleAccess = true + maxErrorRetry = 1 + } +} +// As reported by GET https:///workflows/storage/setup +workDir = 's3:///' +``` + +Run that with `gen3 nextflow run main.nf` and the generated override changes five keys: +`tes.endpoint` and `aws.client.endpoint` become the local proxy, `tes.oauthToken` and +`aws.accessKey` become the task token, and `aws.client.s3PathStyleAccess` is forced on. +`workDir`, `region`, `timeout`, `maxErrorRetry`, `plugins` and `process` are yours and are +left as they are. `gen3 -vv nextflow run` logs the generated file's contents; `nextflow -c + + config` prints the merged result. + +Two details explain why those five: + +- **`accessKey` has to be the task token, and `secretKey` is unused.** Gen3's S3 endpoint + reads the token out of the SigV4 `Credential=/...` field of the header Nextflow + signs. The signature itself is never verified — Gen3 re-signs with its own AWS + credentials — which is why `secretKey` can be any non-empty string. +- **Path-style S3 access is required.** Gen3's S3 endpoint reads the bucket name from the + first segment of the request path, and refuses a request whose first segment is not your + own bucket. Virtual-host-style addressing puts the bucket in the `Host` header instead, + which the proxy has to overwrite with the upstream host — leaving the endpoint to read a + key prefix as the bucket name and answer `403`. + +### Wiring it up yourself + +`--no-generated-config` suppresses the override, at which point your config has to reach +the proxy on its own using the two environment variables: + +```groovy +tes { + endpoint = "http://127.0.0.1:${env('GEN3_DPOP_PROXY_PORT')}/ga4gh/tes" + oauthToken = env('GEN3_DPOP_BOUND_TASK_TOKEN') +} +aws { + accessKey = env('GEN3_DPOP_BOUND_TASK_TOKEN') + secretKey = 'N/A' # pragma: allowlist secret + client { + endpoint = "http://127.0.0.1:${env('GEN3_DPOP_PROXY_PORT')}/s3" + s3PathStyleAccess = true + } +} +``` + +Two things to watch for when writing it by hand: + +- **The S3 endpoint must include `/s3`.** The proxy routes only `/ga4gh/tes` and `/s3`; + pointing the S3 client at the proxy root gets a `404` for every object request. +- **Do not use `def` in a Nextflow config.** The parser rejects variable declarations + alongside config statements (`Variable declarations cannot be mixed with config + statements`), so interpolate the port inline in each endpoint as above. `env()` and + `System.getenv()` both work. + +## Running a pipeline + +```bash +# Basic run (add -v for INFO logs, -vv for DEBUG) +gen3 -v nextflow run main.nf + +# Nextflow's own flags pass straight through +gen3 -v nextflow run main.nf -profile gen3 -resume + +# Ask for a specific task token lifetime, in seconds +gen3 -v nextflow run main.nf --task-token-expiration 3600 + +# Override the proxy's own settings (same options as `gen3 dpop proxy start`) +gen3 -v nextflow run main.nf --port 8000 \ + --tes-endpoint https://gen3.example.com/ga4gh/tes \ + --s3-endpoint https://gen3.example.com/workflows/s3 +``` + +The command exits with Nextflow's exit code, so it can be used in scripts and CI. + +Every flag `gen3 nextflow run` does not recognize is forwarded to `nextflow run`, so +Nextflow's own options and your pipeline's params work as usual. The one exception is a +pipeline param that happens to share a name with an option in `gen3 nextflow run --help` +(`--port` is the plausible one) — put `--` before it and everything after is handed to +Nextflow untouched: + +```bash +gen3 -v nextflow run main.nf -- --port 8080 +``` + +## Running the proxy on its own + +Useful for debugging, or for driving several Nextflow commands against one proxy: + +```bash +# Prints the port it bound to; Ctrl+C to stop +gen3 -v dpop proxy start + +# Pin the port, and/or override discovery of the endpoints +gen3 -v dpop proxy start --port 8000 \ + --tes-endpoint https://gen3.example.com/ga4gh/tes \ + --s3-endpoint https://gen3.example.com/workflows/s3 +``` + +Full option list: `gen3 dpop proxy start --help`. Note that this command only starts the +proxy — it does not export `GEN3_DPOP_PROXY_PORT` or `GEN3_DPOP_BOUND_TASK_TOKEN` into +your shell, so a plain `nextflow run` afterwards has to be given the port some other way. +The simplest is to set the port yourself before starting the proxy, since both sides read +the same variable: + +```bash +export GEN3_DPOP_PROXY_PORT=8000 +gen3 -v dpop proxy start # binds 8000 +nextflow run main.nf # config reads 8000 from the environment +``` + +The task token still has to be supplied separately; only the port is shared this way. + +## Using it from Python + +```python +from gen3.auth import Gen3Auth +from gen3.dpop import dpop_proxy_context + +auth = Gen3Auth() + +with dpop_proxy_context(auth=auth) as (task_token, proxy_port): + # Anything sent to 127.0.0.1:{proxy_port} is signed and forwarded. + ... +``` + +`gen3.dpop_nextflow.run_gen3_nextflow()` is the same thing plus the `nextflow run` child +process. + +## Security properties + +- **The private key never leaves the process.** It is generated in memory per run and is + never written to disk. +- **Proofs are per-request.** Each proxied request gets a fresh proof with a unique + `jti`, bound to the request's method (`htm`) and URL (`htu`) and to the task token + (`ath`). +- **Nonces are server-driven.** When a server answers with `use_dpop_nonce`, the proxy + caches the nonce from the `DPoP-Nonce` response header, re-signs, and retries — up to + two retries per request. It does not manage nonce lifetimes itself; the server decides + when a nonce is stale. +- **Task tokens are scoped.** `--task-token-type` (default `WORKFLOW`) asks Gen3 for a + token limited to that purpose. + +Two limits worth knowing about: + +- **The proxy is an unauthenticated local endpoint.** It listens only on `127.0.0.1`, so + it is not reachable from other machines, but any process running as any user on the + same host can send requests through it while it is up. Only `/ga4gh/tes` and `/s3` + paths are proxied, so that reaches those two services rather than the whole commons + API — but within them it acts with your task token. Treat it like an SSH agent socket: + fine on your own workstation, think twice on a shared host. +- **The task token is not renewed.** It is fetched once at startup. A pipeline that runs + longer than the token's lifetime will start seeing 401s; use `--task-token-expiration` + to request a lifetime that covers the run. +- **The task token cannot outlive your API key.** Gen3 refuses to issue one that would, + so `--task-token-expiration` is capped by whatever is left of the API key. For a long + pipeline, download a fresh API key first. The SDK checks this before it makes the + request and tells you the maximum you can ask for. + +## Troubleshooting + +Every failure below is reported as an `Error: ...` line, not a traceback. A traceback +means an SDK bug — please file it. A missing `nextflow` binary also logs an `ERROR` line +above it explaining the fix. + +| Symptom | What it means | +| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `Requested a task token lifetime of N seconds, but your API key expires in ...` | `--task-token-expiration` exceeds what is left of your API key. Ask for the stated maximum, or download a new API key. | +| `Your API key expired at ...` | Download a new API key from the Gen3 profile page. | +| `Could not get a WORKFLOW task token from ...: [400] ...` | Gen3 rejected the exchange. The text after the status code comes straight from Gen3 and is the actual reason. | +| `Could not find an API key for the DPoP token exchange.` | No API key was found. Check `~/.gen3/credentials.json` or pass `--auth`. | +| `No Gen3 endpoint is configured.` | The credentials do not name a commons. Download a fresh API key. | +| `--tes-endpoint ... is not on - the commons that issued your credentials.` | An endpoint override points at a different host than your API key. Gen3 serves TES and S3 from the same host as the commons, and the task token is only valid there, so the proxy refuses to start rather than send it elsewhere. | +| `Something is already listening on 127.0.0.1:PORT` | Another proxy (or anything else) holds that port. Drop `--port` to get a free one automatically. | +| `Server did not provide a nonce in DPoP-Nonce header.` (HTTP 502) | The server demanded a nonce but sent none. It is a server-side bug; check the Gen3 deployment's DPoP configuration. | +| `502 Upstream request failed` | The proxy could not reach the TES or S3 endpoint. Verify the endpoints with `--tes-endpoint` / `--s3-endpoint`. | +| `504 Upstream timeout` | No progress from the upstream for five minutes, or no connection within 30 seconds. | +| Repeated `401` from TES or S3 | Check the `WWW-Authenticate` value the proxy logs with each rejection: `invalid_token` usually means an expired task token, `invalid_dpop_proof` a proof the server would not accept. | +| Nextflow talks to the commons directly instead of the proxy | With `--no-generated-config` the config has to point at the proxy itself; see [Wiring it up yourself](#wiring-it-up-yourself). Otherwise run with `-vv` and check the generated override — a later `-c` on your own command line would win over it. | +| `404 Not Found` from the proxy on every object request | `aws.client.endpoint` is missing the `/s3` suffix. The proxy routes only `/ga4gh/tes` and `/s3`. See [Wiring it up yourself](#wiring-it-up-yourself). | +| `Bucket misconfigured. Hit the `GET /storage/setup` endpoint and try again.` (400 on the first upload) | Your workflow bucket and its encryption key have not been provisioned yet. Call `GET {commons}/workflows/storage/setup` once, then rerun. The SDK does not call it for you, because it creates cloud resources. | +| `... (bucket '...') not allowed. You can make calls to your personal bucket, '...'` (403) | The first path segment of the S3 request is not your bucket. Either `workDir` names the wrong bucket — the setup endpoint above reports the right one — or `aws.client.s3PathStyleAccess` is not `true`. | +| `Could not find `nextflow` on your PATH.` (with `Error: [Errno 2] ...` after it) | Install Nextflow, or run `gen3 dpop proxy start` and drive Nextflow yourself. | +| `The DPoP proxy needs uvicorn` | Install the extra: `pip install 'gen3[dpop]'`. | + +To see more, run with `-vv`: each proxied request logs its resolved upstream URL, the +response status, any nonce retries, and the full response body of a failed token +exchange. + +## Worker pods + +Task containers launched by Gen3 Workflow do not use this proxy. They authenticate with +their own mechanism (OAuth 2 client credentials) and send no DPoP header. The DPoP-bound +task token and the proxy are strictly client-side. \ No newline at end of file diff --git a/gen3/auth.py b/gen3/auth.py index cc0fbed87..8715c9582 100755 --- a/gen3/auth.py +++ b/gen3/auth.py @@ -71,7 +71,7 @@ def get_access_token_with_key(api_key): """ endpoint = endpoint_from_token(api_key["api_key"]) # attempt to get a token from Fence - auth_url = "{}/user/credentials/cdis/access_token".format(endpoint) + auth_url = "{}/user/credentials/api/access_token".format(endpoint) resp = requests.post(auth_url, json=api_key) token_key = "access_token" return _handle_access_token_response(resp, token_key) @@ -445,6 +445,16 @@ def get_access_token(self): # use cache return self._access_token + def get_api_key(self): + """ + Returns the actual API key loaded, or None if the loaded credentials + do not contain one (for example a raw access token). + """ + if not self._refresh_token: + return None + + return self._refresh_token.get("api_key") + def _get_auth_value(self): """Returns the Authorization header value for the request diff --git a/gen3/cli/__main__.py b/gen3/cli/__main__.py index 995bfda62..d4f475feb 100644 --- a/gen3/cli/__main__.py +++ b/gen3/cli/__main__.py @@ -13,11 +13,13 @@ import gen3.cli.objects as objects import gen3.cli.file as file import gen3.cli.drs_pull as drs_pull +import gen3.cli.dpop as dpop import gen3.cli.users as users import gen3.cli.wrap as wrap import gen3 from gen3 import logging as sdklogging from gen3.cli import nih +from gen3.cli import nextflow class AuthFactory: @@ -143,6 +145,8 @@ def main( main.add_command(drs_pull.drs_pull) main.add_command(file.file) main.add_command(nih.nih) +main.add_command(dpop.dpop) main.add_command(users.users) main.add_command(wrap.run) +main.add_command(nextflow.nextflow) main() diff --git a/gen3/cli/dpop.py b/gen3/cli/dpop.py new file mode 100644 index 000000000..faf448a24 --- /dev/null +++ b/gen3/cli/dpop.py @@ -0,0 +1,98 @@ +"""CLI commands for DPoP proxy management.""" + +import threading + +import click +from cdislogging import get_logger + +from gen3.auth import Gen3Auth +from gen3.dpop import dpop_proxy_context, USER_FACING_ERRORS + +logging = get_logger(__name__) + + +@click.group() +def dpop() -> None: + """DPoP proxy management commands.""" + pass + + +@dpop.group() +def proxy() -> None: + """Manage the DPoP proxy server.""" + pass + + +@proxy.command(name="start") +@click.option( + "--port", + type=int, + default=0, + envvar="GEN3_DPOP_PROXY_PORT", + show_envvar=True, + help="Port to run proxy on. Set ENV VAR, otherwise defaults to an OS-assigned free port.", +) +@click.option( + "--task-token-type", + type=str, + default="WORKFLOW", + show_default=True, + help="Type of task token to request.", +) +@click.option( + "--task-token-expiration", + type=int, + default=None, + help="Task token expiration time in seconds. Server default if omitted.", +) +@click.option( + "--tes-endpoint", + type=str, + default=None, + help="Will default to standard location in the Gen3 instance of your API key.", +) +@click.option( + "--s3-endpoint", + type=str, + default=None, + help="Will default to standard location in the Gen3 instance of your API key.", +) +@click.pass_context +def proxy_start_command( + ctx: click.Context, + port: int, + task_token_type: str, + task_token_expiration: int | None, + tes_endpoint: str | None, + s3_endpoint: str | None, +) -> None: + """ + Start the DPoP proxy server in the foreground. + + Point Nextflow (or another client) at the port and stop the proxy + with Ctrl+C. See docs/nextflow.md for the Nextflow configuration. + """ + stop_event = threading.Event() + try: + auth: Gen3Auth = ctx.obj.get("auth_factory").get() + + with dpop_proxy_context( + auth=auth, + port=port or None, + task_token_type=task_token_type, + task_token_expiration=task_token_expiration, + tes_endpoint=tes_endpoint, + s3_endpoint=s3_endpoint, + ) as (_, proxy_port): + logging.info( + f"*** Starting Gen3 DPoP proxy on http://127.0.0.1:{proxy_port} (Press Ctrl+C to stop)... ***" + ) + try: + stop_event.wait() + except KeyboardInterrupt: + pass + finally: + logging.info("*** Stopped Gen3 DPoP proxy. ***") + except USER_FACING_ERRORS as exc: + # These say what the user should do + raise click.ClickException(str(exc)) from exc diff --git a/gen3/cli/nextflow.py b/gen3/cli/nextflow.py new file mode 100644 index 000000000..e0017cc52 --- /dev/null +++ b/gen3/cli/nextflow.py @@ -0,0 +1,122 @@ +""" +CLI commands for running Nextflow workflows with DPoP-bound task tokens. +""" + +import click +from cdislogging import get_logger + +from gen3.auth import Gen3Auth +from gen3.dpop import USER_FACING_ERRORS +from gen3.dpop_nextflow import run_gen3_nextflow + +logging = get_logger(__name__) + + +@click.group() +def nextflow(): + """Nextflow usage in Gen3""" + pass + + +@nextflow.command( + name="run", + context_settings={"ignore_unknown_options": True, "allow_extra_args": True}, +) +@click.argument("nextflow_args", nargs=-1, type=click.UNPROCESSED) +# The following options mirror `gen3 dpop proxy start`; both hand them to dpop_proxy_context(). +@click.option( + "--port", + type=int, + default=0, + envvar="GEN3_DPOP_PROXY_PORT", + show_envvar=True, + help="Port to run proxy on. Set ENV VAR, otherwise defaults to an OS-assigned free port.", +) +@click.option( + "--task-token-type", + type=str, + default="WORKFLOW", + show_default=True, + help="Type of task token to request.", +) +@click.option( + "--task-token-expiration", + type=int, + default=None, + help="Task token expiration time in seconds. Server default if omitted.", +) +@click.option( + "--tes-endpoint", + type=str, + default=None, + help="Will default to standard location in the Gen3 instance of your API key.", +) +@click.option( + "--s3-endpoint", + type=str, + default=None, + help="Will default to standard location in the Gen3 instance of your API key.", +) +@click.option( + "--no-generated-config", + is_flag=True, + help="Do not generate the config that redirects Nextflow to the proxy. Your own " + "config then has to point tes.endpoint and aws.client.endpoint at " + "127.0.0.1:$GEN3_DPOP_PROXY_PORT itself, and use $GEN3_DPOP_BOUND_TASK_TOKEN as " + "tes.oauthToken and aws.accessKey; see the Nextflow guide in the docs.", +) +@click.pass_context +def nextflow_run( + ctx: click.Context, + nextflow_args: tuple, + port: int, + task_token_type: str, + task_token_expiration: int | None, + tes_endpoint: str | None, + s3_endpoint: str | None, + no_generated_config: bool, +) -> None: + """ + Run Nextflow with a Gen3 DPoP proxy. + + Requires Gen3Auth credentials (API key or refresh token) configured. TES and + S3 endpoints are determined from the credentials. + + Keep your normal Gen3 Nextflow config, the one pointing at your commons. A + small override redirecting TES and S3 to the proxy is generated and passed with + `-c` last, so nothing else in your config changes. + + Anything not listed below is passed through to `nextflow run`. If your + pipeline takes a parameter named like one of these options, put `--` first: + everything after it goes to Nextflow untouched. + + Examples: `gen3 -v nextflow run hello.nf`, `gen3 -v nextflow run main.nf -resume`, + `gen3 -v nextflow run main.nf -- --port 8080` + """ + logging.info("Running Nextflow with DPoP proxy...") + + try: + auth: Gen3Auth = ctx.obj.get("auth_factory").get() + return_code = run_gen3_nextflow( + nf_args=list(nextflow_args), + auth=auth, + port=port or None, + task_token_type=task_token_type, + task_token_expiration=task_token_expiration, + tes_endpoint=tes_endpoint, + s3_endpoint=s3_endpoint, + generate_config=not no_generated_config, + ) + except (*USER_FACING_ERRORS, FileNotFoundError) as exc: + # These say what the user should do + raise click.ClickException(str(exc)) from exc + + if return_code != 0: + logging.error( + f"!!! Nextflow errored. It exited with a non-zero exit code: {return_code}. " + f"See above logs for more error info from Nextflow. !!!" + ) + + # Click discards whatever a command returns, so the exit code has to be set + # explicitly or a failed pipeline would look successful to the caller. + ctx.exit(return_code) diff --git a/gen3/dpop.py b/gen3/dpop.py new file mode 100644 index 000000000..75748a47d --- /dev/null +++ b/gen3/dpop.py @@ -0,0 +1,996 @@ +""" +DPoP utilities for token exchange and local proxy. +""" + +import asyncio +from contextlib import contextmanager +from datetime import datetime, timezone +import json +import socket +import tempfile +import threading +import time +from typing import ( + AbstractSet, + Any, + AsyncIterator, + Dict, + Generator, + Literal, + Tuple, + TYPE_CHECKING, +) + +import httpx2 +import humanfriendly +import requests +from authutils.dpop import generate_dpop_proof +from cdislogging import get_logger +from joserfc import jwk + +from gen3.auth import decode_token, Gen3Auth, Gen3AuthError + +# uvicorn is optional (the `dpop` extra), but gen3.cli.__main__ imports this module +# unconditionally, so it can only be imported lazily inside start_proxy_server. This +# import exists solely so type checkers can resolve that function's return annotation. +if TYPE_CHECKING: + import uvicorn + +logging = get_logger(__name__) + +# httpx2 logs one line per request under its own top-level logger. Reparenting it +# here puts it under `gen3`, so the CLI's -v/-vv flags govern it like everything +# else; `start_proxy_server` does the same for uvicorn's logger. +get_logger("httpx2").parent = logging + +# Proxy should not forward all headers - these are the ones to skip. +# +# Connection-specific fields describe the client's connection to us, not +# ours to the upstream, so an intermediary strips them (RFC 9110 7.6.1). +# +# Framing fields have to be recomputed: uvicorn has already de-chunked the body, so +# forwarding the client's `transfer-encoding` or `content-length` would describe a +# body we are no longer sending (RFC 9112 6.1 and 6.2). +# +# `authorization` / `dpop` are dropped because this proxy replaces them with its own +# credentials - except on the S3 route, see below. +_HEADERS_NOT_FORWARDED = frozenset( + { + b"authorization", + b"connection", + b"content-length", + b"dpop", + b"host", + b"keep-alive", + b"proxy-authenticate", + b"proxy-authorization", + b"te", + b"trailer", + b"transfer-encoding", + b"upgrade", + } +) + +# The S3 route is the exception. Gen3's S3 endpoint expects a SigV4-signed request +# and reads the task token out of the `Credential=/...` field of the +# client's `Authorization` header; it rejects anything it cannot parse that way, +# including the `DPoP` scheme. So on that route the client's header is forwarded +# untouched and the proof travels in the `DPoP` header alone. +_HEADERS_NOT_FORWARDED_TO_S3 = _HEADERS_NOT_FORWARDED - {b"authorization"} + +# The two services the proxy will route to. Broken out here +# to allow expansion in the typing easily if we add more services/endpoints. +_SERVICE_TES = "tes" +_SERVICE_S3 = "s3" +Service = Literal["tes", "s3"] + +# Same idea for the response direction. Content headers are kept, because the +# response body is forwarded raw (still compressed, if it arrived that way). +_RESPONSE_HEADERS_NOT_FORWARDED = frozenset( + { + "connection", + "keep-alive", + "proxy-authenticate", + "trailer", + "transfer-encoding", + "upgrade", + } +) + +# Request bodies are held so a nonce retry can replay them. Anything larger than +# this spills to a temp file instead of sitting in RAM: workflow inputs staged +# through the S3 endpoint can be arbitrarily large. +_MAX_BODY_SIZE_IN_MEMORY = 8 * 1024 * 1024 + +# httpx2 defaults every phase to 5s, which breaks any transfer or task listing +# slower than that. The read/write budgets below are per socket operation rather +# than per request, so a large-but-progressing staging transfer never trips them. +_UPSTREAM_TIMEOUT = httpx2.Timeout(connect=30.0, read=300.0, write=300.0, pool=None) + +_MAX_NONCE_RETRIES = 2 + +# Chunk size for replaying a buffered request body upstream. +_BODY_CHUNK_BYTES = 1024 * 1024 + + +class MissingDPoPNonceError(RuntimeError): + """Raised when a server demands a DPoP nonce without supplying one.""" + + +class ProxyStartupError(RuntimeError): + """Raised when the background DPoP proxy server cannot be started.""" + + +# Failures the user can do something about. Callers that present errors to people +# (the CLI) report these as a message rather than a traceback; anything else is a +# bug and should keep its traceback. +USER_FACING_ERRORS = ( + Gen3AuthError, + ImportError, + MissingDPoPNonceError, + ProxyStartupError, + requests.RequestException, +) + + +def resolve_service_endpoints( + auth: Gen3Auth, + tes_endpoint: str | None = None, + s3_endpoint: str | None = None, +) -> Tuple[str, str]: + """ + Work out which TES and S3 endpoints the proxy should forward to. + + Args: + auth (Gen3Auth): Authenticated Gen3Auth instance. + tes_endpoint (str | None): Override, or None for {endpoint}/ga4gh/tes. + s3_endpoint (str | None): Override, or None for {endpoint}/workflows/s3. + + Returns: + Tuple[str, str]: The TES and S3 endpoints. + + Raises: + Gen3AuthError: If the credentials do not name a commons, or an override + points at a different host than the commons that issued them. + """ + if not auth.endpoint: + raise Gen3AuthError( + "No Gen3 endpoint is configured. Use credentials that name their " + "commons, such as an API key with an `iss` claim." + ) + + tes_endpoint = tes_endpoint or f"{auth.endpoint}/ga4gh/tes" + s3_endpoint = s3_endpoint or f"{auth.endpoint}/workflows/s3" + + # The task token is issued by the commons the credentials name, and Gen3 always + # serves TES and S3 from that same host, so an endpoint anywhere else can only + # be a mistake - and sending the token there would leak it to another domain. + # + # Future NOTE: If we ever support separate hosts for task execution vs storage, + # we'll need to rethink this. For now, this protects the user + # against accidentally sending a task token from Commons A to Commons B + commons_host = httpx2.URL(auth.endpoint).host + for option, endpoint in ( + ("--tes-endpoint", tes_endpoint), + ("--s3-endpoint", s3_endpoint), + ): + if httpx2.URL(endpoint).host != commons_host: + raise Gen3AuthError( + f"{option} is {endpoint}, which is not on {commons_host} - the " + "commons that issued your credentials. The task token is only " + "valid there, so it will not be sent anywhere else." + ) + + return tes_endpoint, s3_endpoint + + +@contextmanager +def dpop_proxy_context( + auth: Gen3Auth, + task_token_type: str = "WORKFLOW", + task_token_expiration: int | None = None, + tes_endpoint: str | None = None, + s3_endpoint: str | None = None, + port: int | None = None, +) -> Generator[Tuple[str, int], None, None]: + """ + Context manager to initialize, run, and cleanly shut down a background DPoP proxy server. + + Handles JWK key generation, API key task token exchange, AsyncDPoPProxy instantiation, + and background server lifecycle cleanup. + + Args: + auth (Gen3Auth): Authenticated Gen3Auth instance. + task_token_type (str): Type of task token to request. Defaults to "WORKFLOW". + task_token_expiration (int | None): Expiration time in seconds for the task token. + tes_endpoint (str | None): GA4GH TES endpoint URL. Defaults to {endpoint}/ga4gh/tes. + s3_endpoint (str | None): S3 service endpoint URL. Defaults to + {endpoint}/workflows/s3. + port (int | None): Specific port to bind to, or None to let the OS assign one. + + Yields: + Tuple[str, int]: A tuple containing (task_token, proxy_port). + + Raises: + Gen3AuthError: If no API key is available, or the token endpoint rejects + the exchange. + ProxyStartupError: If the proxy server cannot be started. + MissingDPoPNonceError: If the token endpoint demands a nonce without + providing one. + requests.RequestException: If the commons cannot be reached. + """ + tes_endpoint, s3_endpoint = resolve_service_endpoints( + auth, tes_endpoint, s3_endpoint + ) + token_endpoint = f"{auth.endpoint}/user/credentials/api/access_token" + + key = jwk.generate_key("EC", "P-256", private=True) + + api_key = auth.get_api_key() + if not api_key: + raise Gen3AuthError( + "Could not find an API key for the DPoP token exchange. Download one from " + "the Gen3 profile page to ~/.gen3/credentials.json, or pass --auth." + ) + + logging.info(f"Found API key! Using endpoint: {auth.endpoint}") + + access_token = auth.get_access_token() + + logging.info( + f"Exchanging API key for task token (type: {task_token_type}, expiration: {task_token_expiration or 'server default'})..." + ) + task_token, nonce = exchange_api_key_for_task_token( + key=key, + token_endpoint=token_endpoint, + api_key=api_key, + task_token_type=task_token_type, + task_token_expiration=task_token_expiration, + additional_headers={"Authorization": f"Bearer {access_token}"}, + ) + logging.info(f"Got task token (type: {task_token_type}) successfully!") + + config = { + "KEY": key, + "TASK_TOKEN": task_token, + "TES_ENDPOINT": tes_endpoint, + "S3_ENDPOINT": s3_endpoint, + "CACHED_NONCE": nonce, + } + + logging.info("Finding port and starting Gen3 DPoP Proxy...") + app = AsyncDPoPProxy(config) + server, thread, proxy_port = start_proxy_server( + app, host="127.0.0.1", port=port, log_level="warning" + ) + + try: + yield task_token, proxy_port + finally: + server.should_exit = True + # The thread runs the shutdown itself; without joining, the listening + # socket can outlive this context manager and leak the port. + thread.join(timeout=10) + if thread.is_alive(): + logging.warning( + "Gen3 DPoP proxy did not shut down within 10 seconds; " + f"port {proxy_port} may stay bound until this process exits." + ) + + +class AsyncDPoPProxy: + """Async ASGI proxy that adds DPoP proof headers to outgoing requests.""" + + def __init__(self, config: dict[str, Any]) -> None: + self._config = config + self._client = httpx2.AsyncClient( + limits=httpx2.Limits(max_connections=None, max_keepalive_connections=100), + timeout=_UPSTREAM_TIMEOUT, + verify=True, + ) + + async def __call__(self, scope: dict[str, Any], receive, send) -> None: + """ASGI application entry point.""" + if scope["type"] == "lifespan": + # Nothing to set up or tear down here. Returning nothing + # to unblock uvicorn startup/shutdown events + return + + if scope["type"] != "http": + await self._send_error(send, 404, "Not Found") + return + + method = scope["method"] + path = scope["path"] + query_string = scope.get("query_string", b"").decode("utf-8") + + route = self._resolve_upstream_url(path, query_string) + if route is None: + await self._send_error(send, 404, "Not Found") + return + + upstream_url, service = route + logging.info(f"upstream_url: {upstream_url}") + + headers = self._build_headers( + method, upstream_url, scope.get("headers", []), service + ) + + with tempfile.SpooledTemporaryFile(max_size=_MAX_BODY_SIZE_IN_MEMORY) as body: + await self._read_body(receive, body) + + try: + response = await self._send_with_nonce_retry( + method, upstream_url, headers, body + ) + except httpx2.TimeoutException as exc: + logging.error(f"Timed out talking to {upstream_url}: {exc}") + await self._send_error(send, 504, f"Upstream timeout: {exc}") + return + except httpx2.HTTPError as exc: + logging.error(f"Could not reach {upstream_url}: {exc}") + await self._send_error(send, 502, f"Upstream request failed: {exc}") + return + except MissingDPoPNonceError as exc: + # this happens if the server doesn't actually give us a nonce - + # it's a server-side error + logging.error(str(exc)) + await self._send_error(send, 502, str(exc)) + return + + await self._send_response(send, response) + + async def aclose(self) -> None: + """Close the shared upstream HTTP client.""" + await self._client.aclose() + + def _resolve_upstream_url( + self, path: str, query_string: str + ) -> tuple[str, Service] | None: + """ + Route a path to its upstream base, or refuse to route it at all. + + Only TES and S3 traffic belongs on this proxy, so the two prefixes are + matched explicitly rather than treating everything else as S3. The + endpoints already end in their prefix, so it is stripped here to avoid + doubling it. + + Args: + path (str): Request path from the ASGI scope, already URL-decoded. + query_string (str): Raw query string, or "" if there was none. + + Returns: + tuple[str, Service] | None: The upstream URL and the service it + belongs to, which decides how the request is authenticated. None + if the path is neither a TES nor an S3 path, or if it escapes the + base it was routed to. + """ + service: Service + if path.startswith("/ga4gh/tes"): + service = _SERVICE_TES + base = self._config["TES_ENDPOINT"].rstrip("/") + subpath = path.removeprefix("/ga4gh/tes") + elif path.startswith("/s3"): + service = _SERVICE_S3 + base = self._config["S3_ENDPOINT"].rstrip("/") + subpath = path.removeprefix("/s3") + else: + logging.warning( + f"Refusing to proxy {path}: only /ga4gh/tes and /s3 paths are " + "proxied. Check the endpoints your pipeline is configured with." + ) + return None + + # httpx2 resolves dot segments when it builds the request, so `/s3/../user` + # would leave the base chosen above and reach an arbitrary commons endpoint + # carrying the task token. Normalize here and confirm it stayed put. + url = httpx2.URL(f"{base}{subpath}") + if not str(url).startswith(base): + logging.warning( + f"Refusing to proxy {path}: it resolves to {url}, which is outside " + f"{base}. The task token is only sent to the TES and S3 endpoints." + ) + return None + + upstream_url = f"{url}?{query_string}" if query_string else str(url) + return upstream_url, service + + def _build_headers( + self, + method: str, + url: str, + request_headers: list[tuple[bytes, bytes]], + service: Service, + ) -> dict[str, str]: + """ + Build request headers with DPoP credentials. + + Both services get a fresh proof in the `DPoP` header. They differ in how + the token itself is carried: TES wants `Authorization: DPoP `, + while Gen3's S3 endpoint reads the token out of the SigV4 `Credential=` + field, so the client's signed `Authorization` is passed through as-is. + + Args: + method (str): The request method, signed into the proof as `htm`. + url (str): The upstream URL, signed into the proof as `htu`. + request_headers (list[tuple[bytes, bytes]]): Raw headers from the + ASGI scope. + service (Service): Which upstream this is going to, from + `_resolve_upstream_url`. + + Returns: + dict[str, str]: Headers to send upstream. + """ + is_s3 = service == _SERVICE_S3 + not_forwarded = ( + _HEADERS_NOT_FORWARDED_TO_S3 if is_s3 else _HEADERS_NOT_FORWARDED + ) + headers = { + k.decode("utf-8"): v.decode("utf-8") + for k, v in request_headers + if k.lower() not in not_forwarded + } + + if not is_s3: + headers["Authorization"] = f"DPoP {self._config['TASK_TOKEN']}" + headers["DPoP"] = self._generate_proof(method, url) + # The Authorization value is a live credential and `-vv` output ends up in + # bug reports, so log the proof (useful, single-use) but never the token. + logging.debug(f"DPoP proof for {method} {url}: {headers['DPoP']}") + + return headers + + def _generate_proof(self, method: str, url: str) -> str: + """Generate a fresh DPoP proof JWT for the given method and URL.""" + return generate_dpop_proof( + key=self._config["KEY"], + method=method, + url=url, + access_token=self._config.get("TASK_TOKEN"), + nonce=self._config.get("CACHED_NONCE"), + ) + + async def _send_with_nonce_retry( + self, + method: str, + url: str, + headers: dict[str, str], + body: tempfile.SpooledTemporaryFile, + ) -> httpx2.Response: + """ + Send the buffered request upstream, retrying nonce rejections. + + Args: + method (str): HTTP method of the incoming request. + url (str): Resolved upstream URL. + headers (dict[str, str]): Headers to send upstream, including the DPoP proof. + body (tempfile.SpooledTemporaryFile): Buffered request body, replayed on retry. + + Returns: + httpx2.Response: Upstream response, still streaming unless its body was + read to classify a nonce error. + + Raises: + MissingDPoPNonceError: If the server asks for a nonce without providing one. + httpx2.HTTPError: If the upstream request cannot be completed. + """ + # Framing has to be explicit: given an iterator body and no content-length, + # httpx2 sends `transfer-encoding: chunked`, which S3-compatible backends + # reject for uploads. Bodyless requests skip the iterator entirely, so they + # go out with no framing headers at all. + body.seek(0, 2) + body_length = body.tell() + if body_length: + headers["content-length"] = str(body_length) + + for attempt in range(_MAX_NONCE_RETRIES + 1): + body.seek(0) + request = self._client.build_request( + method, + url, + content=_aiter_file(body) if body_length else b"", + headers=headers, + ) + response = await self._client.send(request, stream=True) + logging.info(f"Got response. Status Code: {response.status_code}") + if response.status_code >= 400: + # WWW-Authenticate carries the DPoP failure reason (invalid_dpop_proof, + # invalid_token, ...), which is the only clue for a rejected proof. + logging.warning( + f"{method} {url} was rejected with {response.status_code}. " + f"WWW-Authenticate: " + f"{response.headers.get('WWW-Authenticate', '')}" + ) + + if attempt == _MAX_NONCE_RETRIES or not await _is_dpop_nonce_error_async( + response + ): + return response + + logging.info(f"Nonce rejection attempt {attempt + 1}...") + logging.debug(f"Response headers: {response.headers}") + await response.aclose() + + if not response.headers.get("DPoP-Nonce"): + raise MissingDPoPNonceError( + "Server did not provide a nonce in DPoP-Nonce header. " + "It is required to do so for DPoP to work." + ) + + self._config["CACHED_NONCE"] = response.headers["DPoP-Nonce"] + logging.info("updated CACHED_NONCE w/ server-provided nonce") + + headers["DPoP"] = self._generate_proof(method, url) + await asyncio.sleep(0.1 * (attempt + 1)) + + async def _send_response(self, send, response: httpx2.Response) -> None: + """Stream the upstream response back to the ASGI client.""" + try: + if response.is_stream_consumed: + # Classifying a nonce error read and decoded the body, so the + # upstream content headers no longer describe what is forwarded. + await self._send_buffered_response(send, response) + return + + await send( + { + "type": "http.response.start", + "status": response.status_code, + "headers": _forwardable_response_headers(response), + } + ) + async for chunk in response.aiter_raw(): + await send( + {"type": "http.response.body", "body": chunk, "more_body": True} + ) + await send({"type": "http.response.body", "body": b""}) + finally: + await response.aclose() + + async def _send_buffered_response(self, send, response: httpx2.Response) -> None: + """Forward an already-read response, recomputing its content headers.""" + body = response.content + headers = _forwardable_response_headers( + response, drop={"content-encoding", "content-length"} + ) + headers.append((b"content-length", str(len(body)).encode("utf-8"))) + + await send( + { + "type": "http.response.start", + "status": response.status_code, + "headers": headers, + } + ) + await send({"type": "http.response.body", "body": body}) + + async def _send_error(self, send, status: int, message: str) -> None: + """Send a plain-text error response to the ASGI client.""" + body = message.encode("utf-8") + await send( + { + "type": "http.response.start", + "status": status, + "headers": [ + (b"content-type", b"text/plain; charset=utf-8"), + (b"content-length", str(len(body)).encode("utf-8")), + ], + } + ) + await send({"type": "http.response.body", "body": body}) + + @staticmethod + async def _read_body(receive, buffer: tempfile.SpooledTemporaryFile) -> None: + """Read the full request body from the ASGI receive queue into a buffer.""" + more_body = True + while more_body: + message = await receive() + buffer.write(message.get("body", b"")) + more_body = message.get("more_body", False) + + +def exchange_api_key_for_task_token( + key: jwk.Key, + token_endpoint: str, + api_key: str, + task_token_type: str, + task_token_expiration: int | None = None, + additional_headers: Dict[str, str] | None = None, + timeout: float | Tuple[float, float] = (10, 60), +) -> Tuple[str, str | None]: + """ + Exchange API key for DPoP-bound task token. + + Args: + key (jwk.Key): Private JWK instance for signing DPoP proof. + token_endpoint (str): Target token endpoint URL. + api_key (str): Gen3 user API key string. + task_token_type (str): Task token scope/type (e.g., "WORKFLOW"). + task_token_expiration (int | None): Requested token expiration in seconds. + additional_headers (Dict[str, str] | None): Additional headers to include. + timeout (float | Tuple[float, float]): requests timeout, as seconds or + (connect, read) seconds. + + Returns: + Tuple[str, str | None]: A tuple containing (access_token, cached_nonce). + + Raises: + Gen3AuthError: If the requested lifetime cannot outlive the API key, if the + token endpoint returns an error status, or if the response carries no + access token. + MissingDPoPNonceError: If the endpoint demands a nonce without providing one. + requests.RequestException: If the token endpoint cannot be reached. + """ + _check_expiration_fits_api_key(api_key, task_token_expiration) + + headers = {"DPoP": generate_dpop_proof(key=key, method="POST", url=token_endpoint)} + logging.debug(f"Generated DPoP Headers: {headers}") + if additional_headers: + headers.update(additional_headers) + + params: Dict[str, str | int] = {"task_token": task_token_type} + if task_token_expiration is not None: + params["expires_in"] = task_token_expiration + + nonce = None + for attempt in range(_MAX_NONCE_RETRIES + 1): + response = requests.post( + token_endpoint, + json={"api_key": api_key}, + params=params, + headers=headers, + timeout=timeout, + ) + + if not _is_dpop_nonce_error(response): + break + + logging.info( + f"Got invalid nonce error (attempt {attempt + 1} of " + f"{_MAX_NONCE_RETRIES + 1}). Trying with server-provided nonce..." + ) + if not response.headers.get("DPoP-Nonce"): + raise MissingDPoPNonceError( + "Token endpoint asked for a DPoP nonce without providing one in a " + "DPoP-Nonce header. It is required to do so for DPoP to work." + ) + nonce = response.headers["DPoP-Nonce"] + headers["DPoP"] = generate_dpop_proof( + key=key, method="POST", url=token_endpoint, nonce=nonce + ) + + if not response.ok: + logging.debug(f"Token endpoint response body: {response.text}") + raise Gen3AuthError( + f"Could not get a {task_token_type} task token from {response.url}: " + f"[{response.status_code}] {_server_error_message(response)}" + ) + + try: + access_token = response.json()["access_token"] + except (ValueError, KeyError, TypeError): + raise Gen3AuthError( + f"Token endpoint {token_endpoint} returned no access_token for a " + f"{task_token_type} task token. Response body: " + f"{response.text}" + ) + + return access_token, nonce + + +def is_proxy_running(port: int, host: str = "127.0.0.1") -> bool: + """ + Check if a proxy server is running on the given port. + + Args: + port (int): Port number to test. + host (str): Host address to connect to. Defaults to "127.0.0.1". + + Returns: + bool: True if connection succeeded, False otherwise. + """ + try: + with socket.create_connection((host, port), timeout=1): + return True + except (socket.timeout, ConnectionRefusedError, OSError): + return False + + +def start_proxy_server( + app: object, + host: str = "127.0.0.1", + port: int | None = None, + log_level: str = "warning", + timeout: float = 10, +) -> Tuple["uvicorn.Server", threading.Thread, int]: + """ + Start proxy server in a background thread. + + Args: + app (object): ASGI application instance. If it exposes an async + ``aclose()``, it is awaited during shutdown. + host (str): Host address to bind to. Defaults to "127.0.0.1". + port (int | None): Port to bind to, or None to let the OS assign one. + log_level (str): Uvicorn log level. Defaults to "warning". + timeout (float): Startup wait timeout in seconds. Defaults to 10. + + Returns: + Tuple[uvicorn.Server, threading.Thread, int]: A tuple containing + (uvicorn_server, background_thread, actual_bound_port). + + Raises: + ProxyStartupError: If the port is taken, or the server does not come up + within the timeout period. + ImportError: If the `dpop` extra is not installed. + """ + try: + import uvicorn + except ImportError as exc: + raise ImportError( + "The DPoP proxy needs uvicorn, which ships in the `dpop` extra. " + "Install it with: pip install 'gen3[dpop]'" + ) from exc + + # Checked up front because uvicorn reports a bind failure by exiting the thread, + # which reaches the caller as a much vaguer startup error. + if port and is_proxy_running(port, host): + raise ProxyStartupError( + f"Something is already listening on {host}:{port}. Pick another port " + "with --port, or omit it to get an OS-assigned one." + ) + + uvicorn_logger = get_logger("uvicorn") + uvicorn_logger.parent = logging + + server = uvicorn.Server( + uvicorn.Config( + app, + # Port 0 lets the kernel pick a free port atomically; probing for a + # free port first and binding second is a race. + host=host, + port=port or 0, + log_level=log_level, + log_config=None, + ) + ) + + proxy_started = threading.Event() + actual_port: int | None = None + startup_error: BaseException | None = None + + def _run_server() -> None: + nonlocal actual_port, startup_error + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + server.config.load() + server.lifespan = server.config.lifespan_class(server.config) + loop.run_until_complete(server.startup()) + + for sock in server.servers: + actual_port = sock.sockets[0].getsockname()[1] + break + + proxy_started.set() + loop.run_until_complete(server.main_loop()) + except BaseException as exc: # SystemExit if uvicorn cannot bind + startup_error = exc + finally: + proxy_started.set() + try: + # uvicorn only closes its listening sockets in shutdown(); skipping + # it leaks the bound port for the life of the process. + loop.run_until_complete(server.shutdown()) + if hasattr(app, "aclose"): + loop.run_until_complete(app.aclose()) + finally: + loop.close() + + thread = threading.Thread(target=_run_server, daemon=True) + thread.start() + proxy_started.wait(timeout=timeout) + + if actual_port is None: + target = f"{host}:{port}" if port else f"{host} on an OS-assigned port" + reason = ( + f"{type(startup_error).__name__}: {startup_error}" + if startup_error + else f"it did not come up within {timeout} seconds" + ) + raise ProxyStartupError( + f"Could not start the DPoP proxy server on {target} - {reason}. " + "Any uvicorn errors above have the details." + ) from startup_error + + return server, thread, actual_port + + +async def _aiter_file( + buffer: tempfile.SpooledTemporaryFile, +) -> AsyncIterator[bytes]: + """Yield a buffered body in chunks, off the event loop thread.""" + while True: + chunk = await asyncio.to_thread(buffer.read, _BODY_CHUNK_BYTES) + if not chunk: + return + yield chunk + + +def _check_expiration_fits_api_key( + api_key: str, task_token_expiration: int | None +) -> None: + """ + Refuse a task token lifetime the API key cannot cover. + + Gen3 will not issue a token that outlives the API key it was requested with, and + its rejection does not say what lifetime would have worked. The API key is a JWT, + so the answer can be worked out locally before spending a round trip. + + Args: + api_key (str): The Gen3 API key (a JWT) being exchanged. + task_token_expiration (int | None): Requested lifetime in seconds, if any. + + Raises: + Gen3AuthError: If the API key has expired, or expires before the requested + lifetime would. + """ + if not task_token_expiration: + return + + try: + api_key_expiration = decode_token(api_key).get("exp") + except Exception as exc: + # decode_token raises a bare Exception for anything it cannot split or parse. + # An unreadable key is the token endpoint's business, not this check's. + logging.debug(f"Could not read the API key's expiration, skipping check: {exc}") + return + + if not api_key_expiration: + logging.debug("API key carries no `exp` claim, skipping expiration check.") + return + + remaining_seconds = int(api_key_expiration - time.time()) + expires_at = datetime.fromtimestamp(api_key_expiration, tz=timezone.utc) + + if remaining_seconds <= 0: + raise Gen3AuthError( + f"Your API key expired at {expires_at:%Y-%m-%d %H:%M UTC}. " + "Create a new one on the Gen3 profile page and try again." + ) + + if task_token_expiration > remaining_seconds: + raise Gen3AuthError( + f"Requested a task token lifetime of {task_token_expiration} seconds " + f"({humanfriendly.format_timespan(task_token_expiration)}), but your API " + f"key expires in {humanfriendly.format_timespan(remaining_seconds)} " + f"(at {expires_at:%Y-%m-%d %H:%M UTC}). Gen3 will not issue a task token " + f"that outlives the API key: request at most {remaining_seconds} seconds, " + "or create a new API key." + ) + + +def _server_error_message(response: requests.Response) -> str: + """ + Extract the most useful human-readable message from an error response. + + Args: + response (requests.Response): The error response. + + Returns: + str: A server-provided explanation, or the response body if it has no + recognizable error field. + """ + try: + body = response.json() + except ValueError: + return response.text or "" + + if isinstance(body, dict): + for key in ("error_description", "message", "detail", "error"): + value = body.get(key) + if isinstance(value, str) and value: + return value + + return json.dumps(body) + + +def _forwardable_response_headers( + response: httpx2.Response, drop: AbstractSet[str] = frozenset() +) -> list[tuple[bytes, bytes]]: + """ + Encode upstream response headers, dropping ones a proxy must not forward. + + Args: + response (httpx2.Response): Upstream response. + drop (AbstractSet[str]): Extra lowercase header names to omit. + + Returns: + list[tuple[bytes, bytes]]: Header pairs in ASGI form. + """ + return [ + (k.encode("utf-8"), v.encode("utf-8")) + for k, v in response.headers.multi_items() + if k.lower() not in _RESPONSE_HEADERS_NOT_FORWARDED and k.lower() not in drop + ] + + +async def _is_dpop_nonce_error_async(response: httpx2.Response) -> bool: + """ + Check if a streamed response is requesting a DPoP nonce retry. + + Args: + response (httpx2.Response): Streaming HTTP response instance. + + Returns: + bool: True if server requested DPoP nonce retry, False otherwise. + """ + if response.status_code not in (400, 401): + return False + + # The body is only needed for the auth-server style error, and error bodies + # are small; reading it makes the streamed response replayable below. + await response.aread() + return _is_dpop_nonce_error(response) + + +def _is_dpop_nonce_error( + response: httpx2.Response | requests.Response, +) -> bool: + """ + Check if a 400 or 401 response is requesting a DPoP nonce retry. + + Args: + response (httpx2.Response | requests.Response): HTTP response instance. + + Returns: + bool: True if server requested DPoP nonce retry, False otherwise. + """ + if response.status_code not in (400, 401): + return False + + # Check Resource Server (RS) style: WWW-Authenticate header + www_auth = response.headers.get("WWW-Authenticate", "") + if 'error="use_dpop_nonce"' in www_auth or "error='use_dpop_nonce'" in www_auth: + logging.debug( + "DPoP Nonce error found in www-authenticate header (resource server response)" + ) + return True + + # Check Authorization Server (AS) style: Safe JSON body parse + body_error = None + try: + data = response.json() + if isinstance(data, dict): + body_error = next( + ( + data[key] + for key in ("error", "error_description", "message") + if isinstance(data.get(key), str) and data[key] + ), + None, + ) + except ( + ValueError, + httpx2.DecodingError, + requests.exceptions.RequestException, + ): + # Body is empty, non-JSON, or raised a library-specific decoding error + pass + + if body_error == "use_dpop_nonce": + logging.debug("DPoP Nonce error found in response JSON (auth server response)") + return True + + if body_error: + # Servers may attach a DPoP-Nonce to every response on a DPoP endpoint, so the + # header alone cannot be trusted below. Retrying here would burn the retry + # budget and bury the explanation the server just gave. + logging.debug(f"Not a nonce error; server reported: {body_error}") + return False + + # Fallback: Presence of DPoP-Nonce header on a 400/401 error + if "DPoP-Nonce" in response.headers: + logging.info("DPoP Nonce in headers and got a 40x.") + return True + + return False diff --git a/gen3/dpop_nextflow.py b/gen3/dpop_nextflow.py new file mode 100644 index 000000000..f947a50f0 --- /dev/null +++ b/gen3/dpop_nextflow.py @@ -0,0 +1,188 @@ +""" +Demonstrating Proof of Possesssion (DPoP)-bound Task Token and Async Proxy for Nextflow Workflows. + +This module provides client-side support for generating DPoP-bound task tokens +and running a local proxy for Nextflow pipelines. + +Routes both TES and S3 traffic through DPoP. +""" + +import contextlib +import os +from pathlib import Path +import subprocess +import tempfile +from typing import List, Optional + +from cdislogging import get_logger + +from gen3.auth import Gen3Auth +from gen3.dpop import dpop_proxy_context, resolve_service_endpoints + +logging = get_logger(__name__) + +# Written to a temp file and passed to `nextflow run` with `-c` last on every run. +# Nextflow merges a `-c` file over the config the pipeline already has rather than +# replacing it, and the last `-c` wins, so this redirects a config that points +# straight at a commons to the local proxy instead, one key at a time. It sets only +# the keys the proxy forces; workDir, region, executor and plugins stay the user's. +# +# The token is read from the environment rather than written here: a task token is +# a bearer-equivalent credential, and a temp file holding one is readable by every +# process on the host for the life of the run. +_PROXY_CONFIG_TEMPLATE = """\ +// Generated by `gen3 nextflow run` - recreated every run, deleted when it ends. +// +// This overrides only what the DPoP proxy requires. Your own config still owns +// everything else: plugins, executor, container, region, workDir. +// +// The endpoints below are the local proxy, which signs every request with a +// short-lived key held only in memory and forwards it to your commons: +// +// TES {tes_endpoint} +// S3 {s3_endpoint} +// +// To wire this up by hand instead, pass --no-generated-config; see the docs. +tes {{ + endpoint = 'http://127.0.0.1:{port}/ga4gh/tes' + oauthToken = env('GEN3_DPOP_BOUND_TASK_TOKEN') +}} +aws {{ + // Gen3's S3 endpoint reads the token out of the SigV4 `Credential` field, so + // the task token is the access key and the secret is never used. + accessKey = env('GEN3_DPOP_BOUND_TASK_TOKEN') + secretKey = 'N/A' + client {{ + endpoint = 'http://127.0.0.1:{port}/s3' + // Gen3's S3 endpoint reads the bucket name from the first segment of + // the request path. Virtual-host addressing puts it in `Host` instead, + // which the proxy has to overwrite, leaving the endpoint to read a key + // prefix as the bucket name and refuse the request. + s3PathStyleAccess = true + }} +}} +""" + + +def run_gen3_nextflow( + nf_args: List[str], + auth: Gen3Auth, + task_token_type: str = "WORKFLOW", + task_token_expiration: Optional[int] = None, + tes_endpoint: Optional[str] = None, + s3_endpoint: Optional[str] = None, + port: Optional[int] = None, + generate_config: bool = True, +) -> int: + """ + Wrap Nextflow execution with DPoP-bound task token security. + + Generates an ephemeral key, exchanges the user's credentials for a DPoP-bound + task token, starts a local proxy, then runs Nextflow with the task token and + proxy port in its environment as `GEN3_DPOP_BOUND_TASK_TOKEN` and + `GEN3_DPOP_PROXY_PORT`. + + By default it also writes a temporary Nextflow config that redirects TES and S3 + to the proxy and passes it with `-c` last, so a config pointing straight at a + commons runs through the proxy without being edited. Nextflow merges `-c` files + over the existing config, so only the keys the proxy forces change - workDir, + region, executor and plugins stay as the pipeline set them. Pass + `generate_config=False` to point the config at the proxy by hand instead; see + docs/nextflow.md. + + Args: + nf_args (List[str]): Arguments to pass to `nextflow run`. + auth (Gen3Auth): Authenticated Gen3Auth instance with endpoint and credentials. + task_token_type (str): Type of task token to request. Defaults to "WORKFLOW". + task_token_expiration (Optional[int]): Expiration time in seconds for the + task token. The service default is used if omitted. + tes_endpoint (Optional[str]): GA4GH TES endpoint URL. Defaults to + {endpoint}/ga4gh/tes. + s3_endpoint (Optional[str]): S3 service endpoint URL. Defaults to + {endpoint}/workflows/s3. + port (Optional[int]): Port for the proxy, or None for an OS-assigned one. + generate_config (bool): Whether to generate a Nextflow config pointing at + the proxy and pass it with `-c`. Defaults to True. + + Returns: + int: Nextflow process exit code. + + Raises: + Gen3AuthError: If the DPoP-bound token exchange fails. + ProxyStartupError: If the local proxy cannot be started. + FileNotFoundError: If the `nextflow` executable is not on PATH. + """ + # Resolved up front so the generated config can name the real upstreams in its + # header comment, and so a bad --tes-endpoint fails before the token exchange. + resolved_tes_endpoint, resolved_s3_endpoint = resolve_service_endpoints( + auth, tes_endpoint, s3_endpoint + ) + + with dpop_proxy_context( + auth=auth, + task_token_type=task_token_type, + task_token_expiration=task_token_expiration, + tes_endpoint=tes_endpoint, + s3_endpoint=s3_endpoint, + port=port, + ) as (task_token, proxy_port): + env = os.environ.copy() + env["GEN3_DPOP_BOUND_TASK_TOKEN"] = task_token + env["GEN3_DPOP_PROXY_PORT"] = str(proxy_port) + + with contextlib.ExitStack() as stack: + args = list(nf_args) + if generate_config: + # The config outlives nothing: the directory goes away with the run. + directory = stack.enter_context(tempfile.TemporaryDirectory()) + config_path = _write_proxy_config( + directory=directory, + proxy_port=proxy_port, + tes_endpoint=resolved_tes_endpoint, + s3_endpoint=resolved_s3_endpoint, + ) + args += ["-c", config_path] + logging.info(f"Generated Nextflow config: {config_path}") + + logging.info("*** Starting Nextflow run... ***") + try: + result = subprocess.run( + ["nextflow", "run"] + args, env=env, check=False + ) + except FileNotFoundError: + logging.error( + "Could not find `nextflow` on your PATH. Install it and try again." + ) + raise + logging.info("*** Completed Nextflow run! ***") + return result.returncode + + +def _write_proxy_config( + directory: str, + proxy_port: int, + tes_endpoint: str, + s3_endpoint: str, +) -> str: + """ + Write the Nextflow config that redirects TES and S3 to the local proxy. + + Args: + directory (str): Directory to write into; the caller owns its lifetime. + proxy_port (int): Port the DPoP proxy is listening on. + tes_endpoint (str): Upstream TES endpoint, for the comment header. + s3_endpoint (str): Upstream S3 endpoint, for the comment header. + + Returns: + str: Path to the config file that was written. + """ + contents = _PROXY_CONFIG_TEMPLATE.format( + port=proxy_port, + tes_endpoint=tes_endpoint, + s3_endpoint=s3_endpoint, + ) + + path = Path(directory) / "gen3-dpop.config" + path.write_text(contents) + logging.debug(f"Generated Nextflow config {path}:\n{contents}") + return str(path) diff --git a/poetry.lock b/poetry.lock index 6fd949415..d78d9f8b8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. [[package]] name = "aiofiles" @@ -157,7 +157,6 @@ files = [ [package.dependencies] aiohappyeyeballs = ">=2.5.0" aiosignal = ">=1.4.0" -async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" @@ -181,27 +180,6 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" -typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} - -[[package]] -name = "alembic" -version = "1.14.1" -description = "A database migration tool for SQLAlchemy." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5"}, - {file = "alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213"}, -] - -[package.dependencies] -Mako = "*" -SQLAlchemy = ">=1.3.0" -typing-extensions = ">=4" - -[package.extras] -tz = ["backports.zoneinfo ; python_version < \"3.9\"", "tzdata"] [[package]] name = "anyio" @@ -209,34 +187,19 @@ version = "4.11.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, ] [package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] trio = ["trio (>=0.31.0)"] -[[package]] -name = "async-timeout" -version = "5.0.1" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version < \"3.11\"" -files = [ - {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, - {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, -] - [[package]] name = "asyncio" version = "3.4.3" @@ -277,14 +240,14 @@ files = [ [[package]] name = "authlib" -version = "1.6.5" +version = "1.6.12" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main"] files = [ - {file = "authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a"}, - {file = "authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b"}, + {file = "authlib-1.6.12-py2.py3-none-any.whl", hash = "sha256:e9229ad7fde610b139dd12f5edbe97eab9ee78bfb85691247e767727850b99ab"}, + {file = "authlib-1.6.12.tar.gz", hash = "sha256:0656d8482f28fc8221929d5f35b2bde5d13e10555ebc06b4561b0d622e83b1bd"}, ] [package.dependencies] @@ -292,55 +255,37 @@ cryptography = "*" [[package]] name = "authutils" -version = "6.2.6" +version = "8.1.0" description = "Gen3 auth utility functions" optional = false -python-versions = "<4.0,>=3.9" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "authutils-6.2.6-py3-none-any.whl", hash = "sha256:5c0201249aad384f706f9ac981af04eaadcea7e94df415691551e1ae00112106"}, - {file = "authutils-6.2.6.tar.gz", hash = "sha256:3043a2269d8976f73869bf7c40b125d4be8608f870ef76ea27e02727f5ba8500"}, -] +python-versions = ">=3.13, <4.0" +groups = ["main"] +files = [] +develop = false [package.dependencies] -authlib = ">=1.1.0" -cached-property = ">=1.4,<2.0" -cdiserrors = "<2.0.0" -cryptography = ">=41.0.6" +authlib = ">=1.6.6" +cached-property = "~=1.4" +cdiserrors = "*" +cdislogging = "*" +cryptography = ">=48.0.1" httpx = ">=0.23.0,<1.0.0" -pyjwt = {version = ">=2.4.0,<3.0", extras = ["crypto"]} -xmltodict = ">=0.9,<1.0" +joserfc = ">=1.7.3" +pyjwt = {version = ">=2.11.0,<3.0", extras = ["crypto"]} +starlette = "*" +werkzeug = ">=3.1.5" +xmltodict = "~=0.9" [package.extras] -fastapi = ["fastapi (>=0.65.2,<0.66.0)"] -flask = ["Flask (<=2.3.3)"] +datamodelutils = ["datamodelutils"] +fastapi = ["fastapi"] +flask = ["Flask"] -[[package]] -name = "authutils" -version = "6.2.7" -description = "Gen3 auth utility functions" -optional = false -python-versions = "<4.0,>=3.9.2" -groups = ["dev"] -markers = "python_version >= \"3.11\"" -files = [ - {file = "authutils-6.2.7-py3-none-any.whl", hash = "sha256:4f50990b841af56afe3a79a897df269a072035b89440dc27a80451c095cc2005"}, - {file = "authutils-6.2.7.tar.gz", hash = "sha256:768ebf5f1f776e1a78ffe43f730f6fe01e1b5441e7ec9c8f99997284286dac5e"}, -] - -[package.dependencies] -authlib = ">=1.1.0" -cached-property = ">=1.4,<2.0" -cdiserrors = "<2.0.0" -cryptography = ">=44.0.1,<45.0.0" -httpx = ">=0.23.0,<1.0.0" -pyjwt = {version = ">=2.4.0,<3.0", extras = ["crypto"]} -xmltodict = ">=0.9,<1.0" - -[package.extras] -fastapi = ["fastapi (>=0.65.2,<0.66.0)"] -flask = ["Flask (<=2.3.3)"] +[package.source] +type = "git" +url = "https://github.com/uc-cdis/authutils.git" +reference = "feat/dpop" +resolved_reference = "62e0d669a5256fe879df5025d1e3bee810b6707c" [[package]] name = "backoff" @@ -348,31 +293,19 @@ version = "1.11.1" description = "Function decoration for backoff and retry" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "backoff-1.11.1-py2.py3-none-any.whl", hash = "sha256:61928f8fa48d52e4faa81875eecf308eccfb1016b018bb6bd21e05b5d90a96c5"}, {file = "backoff-1.11.1.tar.gz", hash = "sha256:ccb962a2378418c667b3c979b504fdeb7d9e0d29c0579e3b13b86467177728cb"}, ] -[[package]] -name = "blinker" -version = "1.9.0" -description = "Fast, simple object-to-object and broadcast signaling" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, - {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, -] - [[package]] name = "cached-property" version = "1.5.2" description = "A decorator for caching properties in classes." optional = false python-versions = "*" -groups = ["dev"] +groups = ["main"] files = [ {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, @@ -384,7 +317,7 @@ version = "1.0.0" description = "Gen3 shared exceptions and utilities." optional = false python-versions = ">=3.6,<4.0" -groups = ["dev"] +groups = ["main"] files = [ {file = "cdiserrors-1.0.0-py3-none-any.whl", hash = "sha256:2e188645832e8c98468267af3e54bc5d3a298078b9869899256251e54dc1599d"}, {file = "cdiserrors-1.0.0.tar.gz", hash = "sha256:d6e56b0a9f6cc0104f4344f837b735fefb2caf8d0c4ef50569881dc140ebfc4a"}, @@ -402,7 +335,7 @@ version = "1.1.1" description = "Standardized logging tool and format for cdis applications" optional = false python-versions = "*" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "cdislogging-1.1.1.tar.gz", hash = "sha256:77e11648244cda3a8094b8ae6081435a2303f259612846c49ef8825c7be141e3"}, ] @@ -441,7 +374,7 @@ version = "2.0.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main"] markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, @@ -656,22 +589,6 @@ files = [ {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, ] -[[package]] -name = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "click" version = "8.3.0" @@ -679,7 +596,6 @@ description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev"] -markers = "python_version >= \"3.11\"" files = [ {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, @@ -701,127 +617,6 @@ files = [ ] markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} -[[package]] -name = "coverage" -version = "7.10.7" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, - {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, - {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, - {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, - {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, - {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, - {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, - {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, - {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, - {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, - {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, - {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, - {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, - {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, - {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, - {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, - {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, - {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, - {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, - {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, - {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, - {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, - {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, - {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, - {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, - {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] - [[package]] name = "coverage" version = "7.11.1" @@ -829,7 +624,6 @@ description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] -markers = "python_version >= \"3.11\"" files = [ {file = "coverage-7.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:057c0aedcade895c0d25c06daff00fb381dea8089434ec916e59b051e5dead68"}, {file = "coverage-7.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea73d4b5a489ea60ebce592ea516089d2bee8b299fb465fdd295264da98b2480"}, @@ -930,115 +724,65 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "43.0.3" +version = "49.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, - {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, - {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, - {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, - {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, - {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, - {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, -] - -[package.dependencies] -cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "cryptography" -version = "44.0.3" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.7" -groups = ["dev"] -markers = "python_version >= \"3.11\"" -files = [ - {file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"}, - {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"}, - {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"}, - {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"}, - {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"}, - {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"}, - {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"}, - {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"}, - {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"}, - {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"}, - {file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"}, - {file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"}, - {file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"}, - {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"}, - {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"}, - {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"}, - {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"}, - {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"}, - {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"}, - {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"}, - {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"}, - {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"}, - {file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"}, - {file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"}, - {file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"}, - {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"}, - {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"}, - {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"}, - {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"}, - {file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"}, - {file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"}, - {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"}, - {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"}, - {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"}, - {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"}, - {file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"}, - {file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"}, +python-versions = "!=3.9.0,!=3.9.1,>=3.9" +groups = ["main"] +files = [ + {file = "cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68"}, + {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9"}, + {file = "cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f"}, + {file = "cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459"}, + {file = "cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e"}, + {file = "cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866"}, + {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8"}, + {file = "cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3"}, + {file = "cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27"}, + {file = "cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61"}, + {file = "cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8"}, + {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36"}, + {file = "cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e"}, + {file = "cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b"}, + {file = "cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6"}, + {file = "cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6"}, + {file = "cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493"}, ] [package.dependencies] -cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] -pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] -sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] [[package]] name = "dataclasses-json" @@ -1091,7 +835,6 @@ click = ">=8.0.0,<9" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} packaging = ">=23.2" requirements-parser = ">=0.11.0,<1" -tomli = {version = ">=2.0.1", markers = "python_full_version < \"3.11.0\""} [[package]] name = "dictionaryutils" @@ -1112,44 +855,6 @@ jsonschema = "<=4.23.0" PyYAML = "*" requests = "*" -[[package]] -name = "doiclient" -version = "0.1" -description = "" -optional = false -python-versions = "*" -groups = ["dev"] -files = [] -develop = false - -[package.dependencies] -requests = ">=2.5.2,<3.0.0" - -[package.source] -type = "git" -url = "https://github.com/uc-cdis/doiclient" -reference = "1.0.0" -resolved_reference = "1a5f4b2a0b04577f31f3dcec511ade117d9f4ba1" - -[[package]] -name = "dosclient" -version = "0.1" -description = "" -optional = false -python-versions = "*" -groups = ["dev"] -files = [] -develop = false - -[package.dependencies] -requests = ">=2.5.2,<3.0.0" - -[package.source] -type = "git" -url = "https://github.com/uc-cdis/dosclient" -reference = "1.1.0" -resolved_reference = "38c0f1ab42edf3efb1ad6348d7dbdff81b131360" - [[package]] name = "drsclient" version = "0.3.2" @@ -1168,25 +873,6 @@ backoff = ">=1.10.0,<2.0.0" httpx = ">=0.28.1,<1.0.0" requests = ">=2.23.0,<3.0.0" -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "fastavro" version = "1.12.1" @@ -1268,30 +954,6 @@ requests = ">=2.4" [package.extras] tests = ["pytest (>=2.5)", "pytest-cov", "responses"] -[[package]] -name = "flask" -version = "2.3.3" -description = "A simple framework for building complex web applications." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"}, - {file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"}, -] - -[package.dependencies] -blinker = ">=1.6.2" -click = ">=8.1.3" -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} -itsdangerous = ">=2.1.2" -Jinja2 = ">=3.1.2" -Werkzeug = ">=2.3.7" - -[package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] - [[package]] name = "frozenlist" version = "1.8.0" @@ -1432,24 +1094,6 @@ files = [ {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, ] -[[package]] -name = "gen3authz" -version = "1.5.1" -description = "Gen3 authz client" -optional = false -python-versions = ">=3.6,<4.0" -groups = ["dev"] -files = [ - {file = "gen3authz-1.5.1-py3-none-any.whl", hash = "sha256:249ab21471e1ae3c283a30f3e32819db218aaa63d8d7151ac7b6b83496ac3518"}, - {file = "gen3authz-1.5.1.tar.gz", hash = "sha256:897f804a2f9d29181a6074fa14335fc9f18e757dd2e5dcec2a68bb9a008bfcb8"}, -] - -[package.dependencies] -backoff = ">=1.6,<2.0" -cdiserrors = "<2.0.0" -httpx = ">=0.20.0,<1.0.0" -six = ">=1.16.0,<2.0.0" - [[package]] name = "gen3dictionary" version = "2.0.4" @@ -1490,38 +1134,19 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] -[[package]] -name = "hsclient" -version = "0.1" -description = "" -optional = false -python-versions = "*" -groups = ["dev"] -files = [] -develop = false - -[package.dependencies] -requests = ">=2.5.2,<3.0.0" - -[package.source] -type = "git" -url = "https://github.com/uc-cdis/hsclient" -reference = "1.0.0" -resolved_reference = "f122072ee245216da5e4260f718d6f886db81773" - [[package]] name = "httpcore" version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -1537,13 +1162,35 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] trio = ["trio (>=0.22.0,<1.0)"] +[[package]] +name = "httpcore2" +version = "2.9.1" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "httpcore2-2.9.1-py3-none-any.whl", hash = "sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26"}, + {file = "httpcore2-2.9.1.tar.gz", hash = "sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2"}, +] + +[package.dependencies] +h11 = ">=0.16" +truststore = ">=0.10" + +[package.extras] +asyncio = ["anyio (>=4.5.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + [[package]] name = "httpx" version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -1562,6 +1209,32 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "httpx2" +version = "2.9.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "httpx2-2.9.1-py3-none-any.whl", hash = "sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a"}, + {file = "httpx2-2.9.1.tar.gz", hash = "sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a"}, +] + +[package.dependencies] +anyio = ">=4.10" +httpcore2 = "2.9.1" +idna = ">=3.18" +truststore = ">=0.10" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (>=8.4)", "pygments (==2.*)", "rich (>=10,<16)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +ws = ["wsproto (>=1.2)"] +zstd = ["zstandard (>=0.18.0) ; python_version <= \"3.13\""] + [[package]] name = "humanfriendly" version = "10.0" @@ -1579,18 +1252,18 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "idna" -version = "3.11" +version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, ] [package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "importlib-metadata" @@ -1598,12 +1271,11 @@ version = "8.7.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] -markers = {dev = "python_version < \"3.11\""} [package.dependencies] zipp = ">=3.20" @@ -1623,7 +1295,7 @@ version = "2.3.1" description = "" optional = false python-versions = "*" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "indexclient-2.3.1.tar.gz", hash = "sha256:0beaf865aab58112961092aa58d06e31ca1cc8da26e9cd5cf84430d2f6567a0d"}, ] @@ -1631,52 +1303,6 @@ files = [ [package.dependencies] requests = ">=2.5.2,<3.0.0" -[[package]] -name = "indexd" -version = "5.0.4" -description = "Gen3 Indexing Service" -optional = false -python-versions = ">=3.9,<4.0" -groups = ["dev"] -files = [] -develop = false - -[package.dependencies] -alembic = "^1.9.4" -authutils = "^6.0.0" -cdiserrors = "^1.0.0" -cdislogging = "^1.0.0" -doiclient = {git = "https://github.com/uc-cdis/doiclient", rev = "1.0.0"} -dosclient = {git = "https://github.com/uc-cdis/dosclient", rev = "1.1.0"} -flask = "^2.3.3" -gen3authz = "^1.0.4" -hsclient = {git = "https://github.com/uc-cdis/hsclient", rev = "1.0.0"} -indexclient = "^2.1.0" -jsonschema = "^3.2" -psycopg2 = "^2.7" -PyYAML = ">=5.3,<7" -sqlalchemy = "~1.3.3" -sqlalchemy-utils = "^0.37.3" - -[package.source] -type = "git" -url = "https://github.com/uc-cdis/indexd.git" -reference = "5.0.4" -resolved_reference = "5d3e54c31edba946f92d6d9ddccb016a7520e668" - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -1684,41 +1310,28 @@ description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" groups = ["dev"] -markers = "python_version >= \"3.11\"" files = [ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] [[package]] -name = "itsdangerous" -version = "2.2.0" -description = "Safely pass data to untrusted environments and back." +name = "joserfc" +version = "1.7.3" +description = "The ultimate Python library for JOSE RFCs, including JWS, JWE, JWK, JWA, JWT" optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, - {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["dev"] +python-versions = ">=3.10" +groups = ["main"] files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, + {file = "joserfc-1.7.3-py3-none-any.whl", hash = "sha256:7c39f3f2c943dbc03122747fa8ebbd8e156e54904cf25651b452f4d2634a6075"}, + {file = "joserfc-1.7.3.tar.gz", hash = "sha256:116955c2587139dba20621fd0bd7fc9255fa960c9fe7f43c43ebef2e801dcfcf"}, ] [package.dependencies] -MarkupSafe = ">=2.0" +cryptography = ">=45.0.1" [package.extras] -i18n = ["Babel (>=2.7)"] +drafts = ["pycryptodome"] [[package]] name = "jsonschema" @@ -1726,7 +1339,7 @@ version = "3.2.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = "*" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, @@ -1742,33 +1355,13 @@ six = ">=1.11.0" format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] format-nongpl = ["idna", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "webcolors"] -[[package]] -name = "mako" -version = "1.3.10" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, - {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, -] - -[package.dependencies] -MarkupSafe = ">=0.9.2" - -[package.extras] -babel = ["Babel"] -lingua = ["lingua"] -testing = ["pytest"] - [[package]] name = "markupsafe" version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main"] files = [ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, @@ -2052,9 +1645,6 @@ files = [ {file = "multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} - [[package]] name = "mypy-extensions" version = "1.1.0" @@ -2067,62 +1657,6 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] -[[package]] -name = "numpy" -version = "2.0.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "python_version < \"3.11\"" -files = [ - {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, - {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, - {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, - {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, - {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, - {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, - {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, - {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, - {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, - {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, -] - [[package]] name = "numpy" version = "2.3.4" @@ -2130,7 +1664,6 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.11" groups = ["main"] -markers = "python_version >= \"3.11\"" files = [ {file = "numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb"}, {file = "numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f"}, @@ -2286,11 +1819,7 @@ files = [ ] [package.dependencies] -numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, -] +numpy = {version = ">=1.26.0", markers = "python_version >= \"3.12\""} python-dateutil = ">=2.8.2" pytz = ">=2020.1" tzdata = ">=2022.7" @@ -2468,23 +1997,6 @@ files = [ {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, ] -[[package]] -name = "psycopg2" -version = "2.9.11" -description = "psycopg2 - Python-PostgreSQL Database Adapter" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "psycopg2-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:103e857f46bb76908768ead4e2d0ba1d1a130e7b8ed77d3ae91e8b33481813e8"}, - {file = "psycopg2-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:210daed32e18f35e3140a1ebe059ac29209dd96468f2f7559aa59f75ee82a5cb"}, - {file = "psycopg2-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:e03e4a6dbe87ff81540b434f2e5dc2bddad10296db5eea7bdc995bf5f4162938"}, - {file = "psycopg2-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:8dc379166b5b7d5ea66dcebf433011dfc51a7bb8a5fc12367fa05668e5fc53c8"}, - {file = "psycopg2-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:f10a48acba5fe6e312b891f290b4d2ca595fc9a06850fe53320beac353575578"}, - {file = "psycopg2-2.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:6ecddcf573777536bddfefaea8079ce959287798c8f5804bee6933635d538924"}, - {file = "psycopg2-2.9.11.tar.gz", hash = "sha256:964d31caf728e217c697ff77ea69c2ba0865fa41ec20bb00f0977e62fdcc52e3"}, -] - [[package]] name = "py" version = "1.11.0" @@ -2503,7 +2015,7 @@ version = "2.23" description = "C parser in Python" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main"] markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" files = [ {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, @@ -2512,14 +2024,14 @@ files = [ [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.13.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main"] files = [ - {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, - {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, + {file = "pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728"}, + {file = "pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423"}, ] [package.dependencies] @@ -2527,9 +2039,6 @@ cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryp [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pypfb" @@ -2550,7 +2059,6 @@ dictionaryutils = ">=3.4.8" fastavro = ">=1.11.0" gen3 = ">=4.11.3" gen3dictionary = ">=2.0.3" -importlib_metadata = {version = ">=3.6.0", markers = "python_full_version <= \"3.9.0\""} python-json-logger = ">=2.0.0" PyYAML = ">=6.0.1" @@ -2576,7 +2084,7 @@ version = "0.20.0" description = "Persistent/Functional/Immutable data structures" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "pyrsistent-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce"}, {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f"}, @@ -2684,9 +2192,6 @@ files = [ {file = "python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f"}, ] -[package.dependencies] -typing_extensions = {version = "*", markers = "python_version < \"3.10\""} - [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec ; implementation_name != \"pypy\"", "mypy", "orjson ; implementation_name != \"pypy\"", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] @@ -2708,7 +2213,7 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -2846,7 +2351,7 @@ version = "80.9.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, @@ -2867,7 +2372,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -2879,97 +2384,29 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] -name = "sqlalchemy" -version = "1.3.24" -description = "Database Abstraction Library" +name = "starlette" +version = "1.3.1" +description = "The little ASGI library that shines." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["dev"] -files = [ - {file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"}, - {file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"}, - {file = "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl", hash = "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl", hash = "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl", hash = "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl", hash = "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-win32.whl", hash = "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl", hash = "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-win32.whl", hash = "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-win_amd64.whl", hash = "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-win32.whl", hash = "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"}, - {file = "SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"}, -] - -[package.extras] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mysql = ["mysqlclient"] -oracle = ["cx-oracle"] -postgresql = ["psycopg2"] -postgresql-pg8000 = ["pg8000 (<1.16.6)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql (<1) ; python_version < \"3\"", "pymysql ; python_version >= \"3\""] - -[[package]] -name = "sqlalchemy-utils" -version = "0.37.9" -description = "Various utility functions for SQLAlchemy." -optional = false -python-versions = "~=3.4" -groups = ["dev"] +python-versions = ">=3.10" +groups = ["main"] files = [ - {file = "SQLAlchemy-Utils-0.37.9.tar.gz", hash = "sha256:4667edbdcb1ece011076b69772ef524bfbb17cc97e03f11ee6b85d98e7741d61"}, - {file = "SQLAlchemy_Utils-0.37.9-py3-none-any.whl", hash = "sha256:bb6f4da8ac044cb0dd4d0278b1fb434141a5ee9d1881c757a076830ddbb04160"}, + {file = "starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6"}, + {file = "starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0"}, ] [package.dependencies] -six = "*" -SQLAlchemy = ">=1.0" +anyio = ">=3.6.2,<5" [package.extras] -arrow = ["arrow (>=0.3.4)"] -babel = ["Babel (>=1.3)"] -color = ["colour (>=0.0.4)"] -encrypted = ["cryptography (>=0.6)"] -intervals = ["intervals (>=0.7.1)"] -password = ["passlib (>=1.6,<2.0)"] -pendulum = ["pendulum (>=2.0.5)"] -phone = ["phonenumbers (>=5.9.2)"] -test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo ; python_version < \"3.9\"", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "mock (==2.0.0)", "pg8000 (>=1.12.4)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] -test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo ; python_version < \"3.9\"", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "mock (==2.0.0)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] -timezone = ["python-dateutil"] -url = ["furl (>=0.4.1)"] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] [[package]] name = "toml" @@ -2983,59 +2420,6 @@ files = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] -[[package]] -name = "tomli" -version = "2.3.0" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, - {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, - {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, - {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, - {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, - {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, - {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, - {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, - {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, - {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, - {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, - {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, - {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, - {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, -] - [[package]] name = "tqdm" version = "4.67.1" @@ -3058,13 +2442,25 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "truststore" +version = "0.10.4" +description = "Verify certificates using native system trust stores" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981"}, + {file = "truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301"}, +] + [[package]] name = "typing-extensions" version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, @@ -3116,20 +2512,40 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "uvicorn" +version = "0.49.0" +description = "The lightning-fast ASGI server." +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dpop\"" +files = [ + {file = "uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f"}, + {file = "uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=10.4)"] + [[package]] name = "werkzeug" -version = "3.1.3" +version = "3.1.8" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main"] files = [ - {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, - {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, + {file = "werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50"}, + {file = "werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44"}, ] [package.dependencies] -MarkupSafe = ">=2.1.1" +markupsafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] @@ -3140,7 +2556,7 @@ version = "0.13.0" description = "Makes working with XML feel like you are working with JSON" optional = false python-versions = ">=3.4" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, @@ -3297,12 +2713,11 @@ version = "3.23.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, ] -markers = {dev = "python_version < \"3.11\""} [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] @@ -3313,9 +2728,10 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_it type = ["pytest-mypy"] [extras] +dpop = ["uvicorn"] fhir = ["fhirclient"] [metadata] lock-version = "2.1" -python-versions = ">=3.9, <4" -content-hash = "0ef223739ee0f4e9faf3bf60aa2c6ca6ea2757f7b84023e93c838109379a6915" +python-versions = ">=3.13, <4" +content-hash = "247a56da10f5b59f9ded0caab898d67b4a0bf04f16218de44b4cd864fc2b64b6" diff --git a/pyproject.toml b/pyproject.toml index 17982f82b..f47477216 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "gen3" homepage = "https://gen3.org/" -version = "4.28.2" +version = "5.0.0" description = "Gen3 CLI and Python SDK" authors = ["Center for Translational Data Science at the University of Chicago "] license = "Apache-2.0" @@ -13,46 +13,59 @@ classifiers = [ "Intended Audience :: Science/Research", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering", ] [tool.poetry.dependencies] -python = ">=3.9, <4" +python = ">=3.13, <4" requests = "*" -indexclient = "^2.3.0" +httpx2 = ">=2.9.1" +indexclient = ">=2.3.0" drsclient = ">=0.3.0" aiohttp = "*" backoff = "*" -cdislogging = "^1.1.0" +cdislogging = ">=1.1.0" click = "*" importlib_metadata = ">=8,<9" jsonschema = "*" # FIXME updating to >=0.6.0 breaks a few tests dataclasses-json = "<=0.5.9" pypfb = ">=0.6.2" -tqdm = "^4.61.2" +tqdm = ">=4.61.2" humanfriendly ="*" python-dateutil = "*" aiofiles = "*" pandas = ">=1.4.2" -xmltodict = "^0.13.0" +xmltodict = ">=0.13.0" gen3users = "*" +joserfc = ">=1.7.3" + +authutils = {git = "https://github.com/uc-cdis/authutils.git", rev = "feat/dpop"} # A list of all of the optional dependencies, some of which are included in the # below `extras`. They can be opted into by apps. +# fhir fhirclient = { version = "*", optional = true } +# dpop +uvicorn = { version = "*", optional = true } + [tool.poetry.extras] fhir = ["fhirclient"] +dpop = ["uvicorn"] [tool.poetry.group.dev.dependencies] -pytest = "^6.0.0" +pytest = ">=6.0.0" pytest-cov = "*" requests-mock = "*" cdisutilstest = { git = "https://github.com/uc-cdis/cdisutils-test.git", tag = "2.0.0" } -indexd = { git = "https://github.com/uc-cdis/indexd.git", tag = "5.0.4" } -deptry = "^0.23.1" +# Can't be locked while authutils is pinned to the feat/dpop branch (8.1.0): +# indexd 5.0.4 requires authutils <7.0.0. tests/conftest.py still imports it, so +# install it by hand until authutils 8 is released and indexd picks it up. +# TODO FIXME +# indexd = { git = "https://github.com/uc-cdis/indexd.git", tag = "5.0.4" } +deptry = ">=0.23.1" [tool.poetry.scripts] gen3 = "gen3.cli.__main__:main" diff --git a/tests/download_tests/test_download.py b/tests/download_tests/test_download.py index 7440c1f4c..315c62d30 100644 --- a/tests/download_tests/test_download.py +++ b/tests/download_tests/test_download.py @@ -246,7 +246,7 @@ def test_get_drs_object_type(drs_objects, index, expected): assert get_drs_object_type(drs_objects[index]) == expected -@pytest.mark.parametrize("hostname", [("test.datacommons.io")]) +@pytest.mark.parametrize("hostname", ["test.datacommons.io"]) def test_get_external_wts_oidc(wts_oidc, hostname): with requests_mock.Mocker() as m: m.get(f"https://{hostname}/wts/external_oidc/", json=wts_oidc[hostname]) @@ -670,7 +670,7 @@ def test_download_objects( # test download object with no auth m.post( - f"http://{hostname}/user/credentials/cdis/access_token", + f"http://{hostname}/user/credentials/api/access_token", json={}, status_code=404, ) @@ -864,7 +864,7 @@ def test_no_gen3_auth(): with requests_mock.Mocker() as m: m.post( - f"http://{hostname}/user/credentials/cdis/access_token", + f"http://{hostname}/user/credentials/api/access_token", json={}, status_code=501, ) @@ -924,7 +924,7 @@ def test_list_no_auth( with requests_mock.Mocker() as m: m.get(f"https://{hostname}/wts/external_oidc/", json=wts_oidc[hostname]) m.post( - f"http://{hostname}/user/credentials/cdis/access_token", + f"http://{hostname}/user/credentials/api/access_token", json={"access_token": "test_access_token"}, ) for object_id, info in drs_object_info.items(): @@ -937,7 +937,7 @@ def test_list_no_auth( # test getting auth error # test no auth m.post( - f"http://{hostname}/user/credentials/cdis/access_token", + f"http://{hostname}/user/credentials/api/access_token", json={"access_token": "test_access_token"}, status_code=401, ) From dea09cfb5593b783c3238f662636b7c3e046f531 Mon Sep 17 00:00:00 2001 From: Avantol13 Date: Wed, 5 Aug 2026 19:46:55 +0000 Subject: [PATCH 2/7] Apply automatic documentation changes --- docs/_build/doctrees/auth.doctree | Bin 30968 -> 32367 bytes docs/_build/html/_modules/gen3/auth.html | 15 ++++++++++++++- docs/_build/html/auth.html | 8 ++++++++ docs/_build/html/genindex.html | 2 ++ docs/_build/html/index.html | 1 + docs/_build/html/objects.inv | Bin 1894 -> 1900 bytes docs/_build/html/searchindex.js | 2 +- 7 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/_build/doctrees/auth.doctree b/docs/_build/doctrees/auth.doctree index f27f0878f9d8b0933f0cb9fce6e90bdd7835cd03..b550b07813004684035d41b69e6ee3ab53cd2d6c 100644 GIT binary patch delta 915 zcmc&zzfTiU9Oter?Oj`g+LETU@-#8F3AQ8*Xi|`>sXs`JFfb4cy|%BuyS8`7-8H~~ z=zzwg665!rG!Q#DI0+pXj1!BKiG!1a|A2|IF_`FkS1=`Eclo~WeZHUX=f0os-P_;e ze*We>4?Qo-54Jp0av*shTl11G&sXs>RAt1Sfowj~djOzflq2w2&ilJ*Tg7uX`)^=% zB-^k^s;iPn+Qgf;6J#y<$X=nRr`g@;6iN69249o{jLjt>R6PanZ%>Cq#|gtiUB501 zJw3`GPlHagrtgqw9}%>l*veT-Y92cwjH=_S%hT9u7^;aZf{|3R8d42q@7_Xm1*oP; zWn3OYnt|?UDn@d}VRc|sGH@BImMqmwv8*9gvrtJ>ElE}pBPS}f4KGP`y+(zRAuUq* z5;p0Zv?^AUDQ5$#bOrx$gf+k2`8ZK!Nq~h+IygbsN#c_ni7E{NJ{FP?*CzU(v2~C6 zXjo_2cqfC5bcLS~U`5KbXXM8m|5ku0e+<@hQNAU>yW9Y5mx}xchNgWxw6jqp+i=o5 z=u$pWurJN;2%w8Y{8s_q1as_ZDsbMVwCQ;%`oiyqdpN|k;8*Px6U@#=T*_+!QkBd8 zeKv>l1N?y>^*L@6CeC7ls-2A`NC! kP^CJymUV2>TcEJQQMi^E>(h>~0F*5VBJ7uA_7r2qf` delta 273 zcmaF=hw;ZpM%D(_sc$PbvIb^uKAhLaIC*AP=;V0?NsO_R84C?KA~_itN|Q1+O%`OA zXUfU~ipfom=M`d#Rh14W&mdP=xB9q0lQzjoM zvY5OoJB)i4(4Z`NCLpnJa%6Gb$Z+XvEQ%K;?zgIIPD*5n5z_KXgb6-u*tPQiswaRPSource code for gen3.auth
     """
     endpoint = endpoint_from_token(api_key["api_key"])
     # attempt to get a token from Fence
-    auth_url = "{}/user/credentials/cdis/access_token".format(endpoint)
+    auth_url = "{}/user/credentials/api/access_token".format(endpoint)
     resp = requests.post(auth_url, json=api_key)
     token_key = "access_token"
     return _handle_access_token_response(resp, token_key)
@@ -486,6 +486,19 @@ 

Source code for gen3.auth

         return self._access_token
+
+[docs] + def get_api_key(self): + """ + Returns the actual API key loaded, or None if the loaded credentials + do not contain one (for example a raw access token). + """ + if not self._refresh_token: + return None + + return self._refresh_token.get("api_key")
+ + def _get_auth_value(self): """Returns the Authorization header value for the request diff --git a/docs/_build/html/auth.html b/docs/_build/html/auth.html index 4c24f3929..74fd6562b 100644 --- a/docs/_build/html/auth.html +++ b/docs/_build/html/auth.html @@ -124,6 +124,13 @@

Gen3 Auth Helper +
+get_api_key()[source]
+

Returns the actual API key loaded, or None if the loaded credentials +do not contain one (for example a raw access token).

+
+
refresh_access_token(endpoint=None)[source]
@@ -173,6 +180,7 @@

Gen3 SDK

  • Gen3Auth.curl()
  • Gen3Auth.get_access_token()
  • Gen3Auth.get_access_token_from_wts()
  • +
  • Gen3Auth.get_api_key()
  • Gen3Auth.refresh_access_token()
  • diff --git a/docs/_build/html/genindex.html b/docs/_build/html/genindex.html index fc7b36ca6..865e318fc 100644 --- a/docs/_build/html/genindex.html +++ b/docs/_build/html/genindex.html @@ -349,6 +349,8 @@

    G

  • get_aliases() (gen3.metadata.Gen3Metadata method)
  • get_all_records() (gen3.index.Gen3Index method) +
  • +
  • get_api_key() (gen3.auth.Gen3Auth method)
  • get_dictionary_all() (gen3.submission.Gen3Submission method)
  • diff --git a/docs/_build/html/index.html b/docs/_build/html/index.html index d329e8bcb..8137650a4 100644 --- a/docs/_build/html/index.html +++ b/docs/_build/html/index.html @@ -43,6 +43,7 @@

    Welcome to Gen3 SDK’s documentation!Gen3Auth.curl()
  • Gen3Auth.get_access_token()
  • Gen3Auth.get_access_token_from_wts()
  • +
  • Gen3Auth.get_api_key()
  • Gen3Auth.refresh_access_token()
  • diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv index 8f2fb369e10a277a49c29de1f9aeb6fd64436a33..8b30925017a68664f54e51ae219a652d05f4589f 100644 GIT binary patch delta 1801 zcmV+k2ln{p4(tw)cz?@w+cps0>nqgLZq2r*C!6eCNlBbqmZL|LW`hHfkjR)M7yz`a zuKFAOy?#mYB1KBHBof1&M9jSd3}yxd2HcYLHxK6`^xn%~xAMm@+rRZ=%=z%wp>7C8 z694$q@Mf?*k@ftX#R`xR5z^oq#FhwzeV=fS1brf@8yamGO@Gi%@D4Otgr3O09gvX? zBRp(1f=v_?&G1hCs9IMyoRRec5)(nt#^OpPZGb4IJ{A#8c}ohqOJj-y#G`cER==`u zCJhyBQxc$KAGrw6Gp)J(xGO+8H)xwj!6kwy4akdTxKqEX^P`*O=+7fO%=f8}d`7S| zIU_!0b)~Inlz-*mIJk)qKP3JW&l4C*nxYJEi5uB25}~AFIN{xizUPGPm3ujGF;1Tx@G04$XTrFQR0o#s!>}o%JAs_ljxdDdxbUv76e`rY$~l|pr7K4)Ey#t- zEVxDQ0gVFcyD@M_xJ#5P8ZNNt+=s3at+3BHjsoKjoqs9Z9Kgamt9ML|5l11xu?Y8F z&~7Q!CezNjuyr#2M%P^9Kgu7~wOcowk+s>M#R2P6bxZ+vA#?`XcZZU%H5kBbi6P*(o^(lWkZ|Io8mr{ZpOCQs4?)?MHe5tgNK-);;KNZ$0 z6q=^9;@+0wEc_m$Q&V4q?uKG`hm`S9aR_#if#>Ta;>sa4&7%BU9d1SFN_3HFPrpvfBcH?CyPW+N{dWddY2EfvftRjv;rU!7oM zxqmaoaSY4|F=BlJWfK+mLy}+!M(e^Hh)X$eW#Evk2~ML8;R2A;dDJu`DOB8^E+ZXC zbc;BxYILV+6JdfyBVbBtMOxi`ToJ z?38oDYz-Aw!J<4J1M?$8srF$=Lp6Jz5(qHFI-hV~r z;zDoqVxRTY|14wnvbW1%8OuX@;Hi8q?)T{+2pC-RJv%<6+^o$ z_o+fXIhNzpWHg(P(c|s=30mBJoxm!Y-hM%&*+Vgi`Q+1TvRE!)l`N-^=-upoJOQ?lt>YHY zZOoo^o9k4W3Ja9a)Y-h-(Y=#>kLgy+4_^WKfc*K0_5JE@3`jQ}zX4=>oPSULM8G|C z4b>yMWhh@)f)Pyxqw(omRwvHY9MloZ!f$-IHU<)Z_Agv`KNA*h_F(aSLSGlqq1va;_AYCuQb1h@J!9{BnYG#8uhDs<+5KwzfR;0~T1?RG;!G&NQqH}gK=GmF zUyl>Hip`N;mnUQN?(TkqW`DnboQ#(JDSMbb^sTU!Uv*Pq{Hl0rrOtbni=xBjOLK2u zduh-{&5oN}c%S(~P-gG%7AaRXUJ#dC*Q{RXxCYdCRMZA|@`=bb;G-+5*FjEi<6MJd zNe4(bRpt|xjaFos76%Z8#ai?j=XFidy@1}?2`C?d{8P*7z{HEbW rz!*%)Rl!Yu(U61>j$yzu^s0y=63ys7Q})C&?31Al;KBdf2?CfIJRpjF delta 1794 zcmV+d2mSc$4(1M!cz@lNn>G->_fxp1y-EMzO`01W8Dv8i5Rt@s z-K)MvU$0M6{ei(|ZG(n88_oBP7|o0jGi*uvn}>4|dhg}0Tlr&{?ce$_=6v|;P!EJ6 ziGTcQcr)0Z$a;RxVg*Qu2x)K)VoL3$nFWppQytJ)EUa2yxj)s;epI#)So6TNihh@}O&a9IVn=sloO zKz%m`?g)2@az(=h7MsSh0fvY`<)iifzky}%QIpMBs zc6Zfmkbhk6Gdte4yHP;R#v}k37JxbBDibsM90i0a8Y~2h*14QvOQtmze(3C(FnD*n zSRS2|(G%Ir-O3#QyU`hkcE!5}7gShT9w_6GG{|U_3Mcn-3UZL}RwQ+kIFdE(XRSzH z+1pxi=%=ObaMx_tKF`thH{uJ;GSfe+d#xS3fPW|)oR;`fX(NHQk;Z>2tW_v9LubXk zEyG#(Jw~Udz6RY5#qbU(((6BYMEl3)l%>%ts}OF3}m#vxe~oJJeM1t6#MUDJ%DP;q;@jdUQ_ z*esiTK2x)+dl#6l#((DpFm@Fkru=wXcLY-+`Dy%IJl^$Wr<@aJ zYpAda7UkP9Fh4Ss>Kukdl#t_d7!*GjSnioUYp}KfWykA!RU3rRQ(H=r62PbH+48;d zyM9dK`C$|X%!FwV?D|o@CzsS3`pIRQTG!`O+}F%CJ&*qGiC%3b{Y~EFU1S;;dVixA z`>dz=XBo3^tAL%xyzBt>_-WY`d;y(lafr7IsVXfn#x51WM`xWH zG%`zSxKvf6Y(Cn!b0@(Y+mHim2Ke+*HiD_e<0wjz8m^jUlc)Mn>7qqf4DGhurwVoD z@a7VX7RWYbmAf>0&Hd`fu6jwl`G45;I-xgO&F7PcB^uw&n|>e*fOOOG8$hdvjK^?Iy{Kkj2F_8GPf8oOWnXqWH2aE3`I;aRbrtKkBAFD74 z)n0YBcUe1?0;(bOjJ@k+)@FOZM(2%Y_p9jxTF%gFF+sPBGok!SIroAB#fO%EJx=5< zHb;72o{Z7EyZZ^6{r+(>T7UMZ>|yrMx58F_)lG%*tKzDaI`3I7iVn?}rf*++Y0yT^ zj+-vL&wL>$v;Dh8%2k6G#O2X7s~0-10X4oVY6Cp^MC2OqQH$zzkkf9QYj7;-0O_X6 ze8SQZcHWMvoo&cP@K$w~-&DR;8Y-@#{)X!Vi8I0mx=2+KEkB$wqJR2Jrjc?hH6QJ! zS~|vS5-a)Qs=?^y-oZi4l2#->lh~&Tk;bf* zr_woNwAk((qocxhL3_>PUM_rE*Jg^B6=jD87Ng7bvgfszBQHJct@SrW{#;9jcKAu~h#D^)vH{eEl>%9M2cS;+ Date: Thu, 20 Aug 2026 16:05:47 -0500 Subject: [PATCH 3/7] feat(tests): dpop tests, rip out indexd dependency by mocking, parallelize tests --- .gitignore | 11 + .secrets.baseline | 28 +- gen3/cli/dpop.py | 2 +- poetry.lock | 2804 +++++++++-------- pyproject.toml | 29 +- tests/conftest.py | 122 +- tests/dpop/__init__.py | 0 tests/dpop/conftest.py | 9 + tests/dpop/test_dpop.py | 1261 ++++++++ tests/dpop/test_dpop_nextflow.py | 368 +++ tests/fake_indexd.py | 410 +++ tests/test_import.py | 5 - tests/test_manifests.py | 40 +- .../test_is_valid_manifest_format.py | 16 +- 14 files changed, 3608 insertions(+), 1497 deletions(-) create mode 100644 tests/dpop/__init__.py create mode 100644 tests/dpop/conftest.py create mode 100644 tests/dpop/test_dpop.py create mode 100644 tests/dpop/test_dpop_nextflow.py create mode 100644 tests/fake_indexd.py diff --git a/.gitignore b/.gitignore index 2b0627bb2..74e5b3541 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,17 @@ schema.json tests/outputs *.sq3 gen3/tools/indexing/tmp/ +gen3tests-*.logs + +# nextflow +.nextflow +.nextflow.log* + +# AI +.claude +.serena +AGENTS.md +CLAUDE.md .dccache .idea diff --git a/.secrets.baseline b/.secrets.baseline index 3bc771eed..50452d948 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -90,6 +90,10 @@ { "path": "detect_secrets.filters.allowlist.is_line_allowlisted" }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", "min_level": 2 @@ -593,77 +597,77 @@ "filename": "tests/test_manifests.py", "hashed_secret": "6487fa7aea293dfde99cda9c6ccb9161219331c7", "is_verified": false, - "line_number": 110 + "line_number": 114 }, { "type": "Hex High Entropy String", "filename": "tests/test_manifests.py", "hashed_secret": "e1da93616713812cb50e0ac845b1e9e305d949f1", "is_verified": false, - "line_number": 133 + "line_number": 137 }, { "type": "Hex High Entropy String", "filename": "tests/test_manifests.py", "hashed_secret": "47f42f4c34fddab383b817e689dc0fb75af81266", "is_verified": false, - "line_number": 141 + "line_number": 145 }, { "type": "Hex High Entropy String", "filename": "tests/test_manifests.py", "hashed_secret": "300d95dd5d30ab6928ffda6c08c6a129a23e5b39", "is_verified": false, - "line_number": 150 + "line_number": 154 }, { "type": "Hex High Entropy String", "filename": "tests/test_manifests.py", "hashed_secret": "96c9184fb19c9c1618ccf44d141f8029a739891c", "is_verified": false, - "line_number": 159 + "line_number": 163 }, { "type": "Hex High Entropy String", "filename": "tests/test_manifests.py", "hashed_secret": "7c35c215b326b9463b669b657c1ff9873ff53d9a", "is_verified": false, - "line_number": 642 + "line_number": 646 }, { "type": "Hex High Entropy String", "filename": "tests/test_manifests.py", "hashed_secret": "f9e664db75c7f23a299b0b055c10e08d47073e93", "is_verified": false, - "line_number": 677 + "line_number": 681 }, { "type": "Hex High Entropy String", "filename": "tests/test_manifests.py", "hashed_secret": "14bafb268df75cc8f8d8f87f61468d249de936ba", "is_verified": false, - "line_number": 809 + "line_number": 813 }, { "type": "Hex High Entropy String", "filename": "tests/test_manifests.py", "hashed_secret": "bce350451d95a06da0423916d228575c633008ef", "is_verified": false, - "line_number": 817 + "line_number": 821 }, { "type": "Hex High Entropy String", "filename": "tests/test_manifests.py", "hashed_secret": "0d515eaf06062d52e8c80abb4d3b713a65396d30", "is_verified": false, - "line_number": 900 + "line_number": 904 }, { "type": "Hex High Entropy String", "filename": "tests/test_manifests.py", "hashed_secret": "b4cff7c2af45cdfe66195ec574a7b8832f8621ea", "is_verified": false, - "line_number": 905 + "line_number": 909 } ], "tests/test_submission.py": [ @@ -676,5 +680,5 @@ } ] }, - "generated_at": "2026-08-05T19:45:34Z" + "generated_at": "2026-08-20T21:05:41Z" } diff --git a/gen3/cli/dpop.py b/gen3/cli/dpop.py index faf448a24..1afb8b726 100644 --- a/gen3/cli/dpop.py +++ b/gen3/cli/dpop.py @@ -84,7 +84,7 @@ def proxy_start_command( tes_endpoint=tes_endpoint, s3_endpoint=s3_endpoint, ) as (_, proxy_port): - logging.info( + logging.warning( f"*** Starting Gen3 DPoP proxy on http://127.0.0.1:{proxy_port} (Press Ctrl+C to stop)... ***" ) try: diff --git a/poetry.lock b/poetry.lock index d78d9f8b8..7cc3551a5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,144 +14,143 @@ files = [ [[package]] name = "aiohappyeyeballs" -version = "2.6.1" +version = "2.7.1" description = "Happy Eyeballs for asyncio" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, + {file = "aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472"}, + {file = "aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d"}, ] [[package]] name = "aiohttp" -version = "3.13.2" +version = "3.14.3" description = "Async http client/server framework (asyncio)" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155"}, - {file = "aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c"}, - {file = "aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802"}, - {file = "aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f"}, - {file = "aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6"}, - {file = "aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251"}, - {file = "aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514"}, - {file = "aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0"}, - {file = "aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb"}, - {file = "aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592"}, - {file = "aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782"}, - {file = "aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8"}, - {file = "aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec"}, - {file = "aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c"}, - {file = "aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b"}, - {file = "aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc"}, - {file = "aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e"}, - {file = "aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169"}, - {file = "aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248"}, - {file = "aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e"}, - {file = "aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45"}, - {file = "aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be"}, - {file = "aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742"}, - {file = "aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e"}, - {file = "aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476"}, - {file = "aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23"}, - {file = "aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254"}, - {file = "aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a"}, - {file = "aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b"}, - {file = "aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61"}, - {file = "aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011"}, - {file = "aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4"}, - {file = "aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a"}, - {file = "aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940"}, - {file = "aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4"}, - {file = "aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673"}, - {file = "aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd"}, - {file = "aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e"}, - {file = "aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be"}, - {file = "aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c"}, - {file = "aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734"}, - {file = "aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f"}, - {file = "aiohttp-3.13.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7fbdf5ad6084f1940ce88933de34b62358d0f4a0b6ec097362dcd3e5a65a4989"}, - {file = "aiohttp-3.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7c3a50345635a02db61792c85bb86daffac05330f6473d524f1a4e3ef9d0046d"}, - {file = "aiohttp-3.13.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e87dff73f46e969af38ab3f7cb75316a7c944e2e574ff7c933bc01b10def7f5"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2adebd4577724dcae085665f294cc57c8701ddd4d26140504db622b8d566d7aa"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e036a3a645fe92309ec34b918394bb377950cbb43039a97edae6c08db64b23e2"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:23ad365e30108c422d0b4428cf271156dd56790f6dd50d770b8e360e6c5ab2e6"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f9b2c2d4b9d958b1f9ae0c984ec1dd6b6689e15c75045be8ccb4011426268ca"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a92cf4b9bea33e15ecbaa5c59921be0f23222608143d025c989924f7e3e0c07"}, - {file = "aiohttp-3.13.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:070599407f4954021509193404c4ac53153525a19531051661440644728ba9a7"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:29562998ec66f988d49fb83c9b01694fa927186b781463f376c5845c121e4e0b"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4dd3db9d0f4ebca1d887d76f7cdbcd1116ac0d05a9221b9dad82c64a62578c4d"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d7bc4b7f9c4921eba72677cd9fedd2308f4a4ca3e12fab58935295ad9ea98700"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dacd50501cd017f8cccb328da0c90823511d70d24a323196826d923aad865901"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8b2f1414f6a1e0683f212ec80e813f4abef94c739fd090b66c9adf9d2a05feac"}, - {file = "aiohttp-3.13.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04c3971421576ed24c191f610052bcb2f059e395bc2489dd99e397f9bc466329"}, - {file = "aiohttp-3.13.2-cp39-cp39-win32.whl", hash = "sha256:9f377d0a924e5cc94dc620bc6366fc3e889586a7f18b748901cf016c916e2084"}, - {file = "aiohttp-3.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:9c705601e16c03466cb72011bd1af55d68fa65b045356d8f96c216e5f6db0fa5"}, - {file = "aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32"}, + {file = "aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7"}, + {file = "aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19"}, + {file = "aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71"}, + {file = "aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf"}, + {file = "aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7"}, + {file = "aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc"}, ] [package.dependencies] @@ -164,7 +163,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\" and sys_platform != \"android\" and sys_platform != \"ios\"", "aiodns (>=3.3.0) ; sys_platform != \"android\" and sys_platform != \"ios\"", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\" and sys_platform != \"android\" and sys_platform != \"ios\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] [[package]] name = "aiosignal" @@ -183,79 +182,53 @@ frozenlist = ">=1.1.0" [[package]] name = "anyio" -version = "4.11.0" +version = "4.14.2" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, - {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, + {file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"}, + {file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"}, ] [package.dependencies] idna = ">=2.8" -sniffio = ">=1.1" [package.extras] -trio = ["trio (>=0.31.0)"] - -[[package]] -name = "asyncio" -version = "3.4.3" -description = "reference implementation of PEP 3156" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "asyncio-3.4.3-cp33-none-win32.whl", hash = "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de"}, - {file = "asyncio-3.4.3-cp33-none-win_amd64.whl", hash = "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c"}, - {file = "asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"}, - {file = "asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41"}, -] - -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["dev"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] +trio = ["trio (>=0.32.0)"] [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ - {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, - {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] [[package]] name = "authlib" -version = "1.6.12" +version = "1.7.2" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "authlib-1.6.12-py2.py3-none-any.whl", hash = "sha256:e9229ad7fde610b139dd12f5edbe97eab9ee78bfb85691247e767727850b99ab"}, - {file = "authlib-1.6.12.tar.gz", hash = "sha256:0656d8482f28fc8221929d5f35b2bde5d13e10555ebc06b4561b0d622e83b1bd"}, + {file = "authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f"}, + {file = "authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231"}, ] [package.dependencies] cryptography = "*" +joserfc = ">=1.6.0" [[package]] name = "authutils" -version = "8.1.0" +version = "9.0.0" description = "Gen3 auth utility functions" optional = false python-versions = ">=3.13, <4.0" @@ -269,7 +242,7 @@ cached-property = "~=1.4" cdiserrors = "*" cdislogging = "*" cryptography = ">=48.0.1" -httpx = ">=0.23.0,<1.0.0" +httpx2 = ">=2.9.1" joserfc = ">=1.7.3" pyjwt = {version = ">=2.11.0,<3.0", extras = ["crypto"]} starlette = "*" @@ -285,18 +258,18 @@ flask = ["Flask"] type = "git" url = "https://github.com/uc-cdis/authutils.git" reference = "feat/dpop" -resolved_reference = "62e0d669a5256fe879df5025d1e3bee810b6707c" +resolved_reference = "86cdf9a8df69edefcc70be1fd302dfd59305292d" [[package]] name = "backoff" -version = "1.11.1" +version = "2.2.1" description = "Function decoration for backoff and retry" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7,<4.0" groups = ["main"] files = [ - {file = "backoff-1.11.1-py2.py3-none-any.whl", hash = "sha256:61928f8fa48d52e4faa81875eecf308eccfb1016b018bb6bd21e05b5d90a96c5"}, - {file = "backoff-1.11.1.tar.gz", hash = "sha256:ccb962a2378418c667b3c979b504fdeb7d9e0d29c0579e3b13b86467177728cb"}, + {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, + {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, ] [[package]] @@ -313,154 +286,156 @@ files = [ [[package]] name = "cdiserrors" -version = "1.0.0" +version = "2.1.0" description = "Gen3 shared exceptions and utilities." optional = false -python-versions = ">=3.6,<4.0" +python-versions = "<4,>=3.13" groups = ["main"] files = [ - {file = "cdiserrors-1.0.0-py3-none-any.whl", hash = "sha256:2e188645832e8c98468267af3e54bc5d3a298078b9869899256251e54dc1599d"}, - {file = "cdiserrors-1.0.0.tar.gz", hash = "sha256:d6e56b0a9f6cc0104f4344f837b735fefb2caf8d0c4ef50569881dc140ebfc4a"}, + {file = "cdiserrors-2.1.0-py3-none-any.whl", hash = "sha256:702a15e3831f85b4d8b1cd4f66f8581cbe43faf6bb22d1726e657e76cf6d238b"}, + {file = "cdiserrors-2.1.0.tar.gz", hash = "sha256:853f807111f188d26031d553bfd2b70fe7d23826304cba324aebc40e6442fd2d"}, ] [package.dependencies] -cdislogging = ">=1.0.0,<2.0.0" +cdislogging = "*" +werkzeug = "*" [package.extras] -flask = ["Flask (>=1.1.2,<2.0.0)"] +flask = ["Flask"] [[package]] name = "cdislogging" -version = "1.1.1" -description = "Standardized logging tool and format for cdis applications" +version = "1.2.0" +description = "Standardized logging tool and format for CTDS applications" optional = false -python-versions = "*" +python-versions = "<4,>=3.13" groups = ["main"] files = [ - {file = "cdislogging-1.1.1.tar.gz", hash = "sha256:77e11648244cda3a8094b8ae6081435a2303f259612846c49ef8825c7be141e3"}, + {file = "cdislogging-1.2.0-py3-none-any.whl", hash = "sha256:3596addc972c3e65c738231381244089f84466e9c6e2662e096ad53154018f54"}, + {file = "cdislogging-1.2.0.tar.gz", hash = "sha256:ca7511b685bbf0b4727fef85ca4eab78271b4618139a621b09c2846aed6d19d1"}, ] -[[package]] -name = "cdisutilstest" -version = "2.0.0" -description = "Collection of test data and tools" -optional = false -python-versions = "*" -groups = ["dev"] -files = [] -develop = false - -[package.source] -type = "git" -url = "https://github.com/uc-cdis/cdisutils-test.git" -reference = "2.0.0" -resolved_reference = "74a607736ca4af5ec35f17830ab9b78b5db15837" - [[package]] name = "certifi" -version = "2025.10.5" +version = "2026.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, - {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.1" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9"}, + {file = "cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41"}, + {file = "cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa"}, + {file = "cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3"}, + {file = "cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0"}, + {file = "cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735"}, + {file = "cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e"}, + {file = "cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a"}, + {file = "cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7"}, + {file = "cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac"}, + {file = "cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d"}, + {file = "cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13"}, + {file = "cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c"}, + {file = "cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48"}, + {file = "cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f"}, + {file = "cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4"}, + {file = "cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e"}, + {file = "cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7"}, + {file = "cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac"}, + {file = "cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960"}, + {file = "cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5"}, + {file = "cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66"}, + {file = "cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3"}, + {file = "cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692"}, + {file = "cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be"}, ] [package.dependencies] @@ -468,137 +443,196 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.5.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win32.whl", hash = "sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_arm64.whl", hash = "sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8"}, + {file = "charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6"}, + {file = "charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3"}, ] [[package]] name = "click" -version = "8.3.0" +version = "8.4.2" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, - {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, + {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, + {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, ] [package.dependencies] @@ -619,104 +653,133 @@ markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \" [[package]] name = "coverage" -version = "7.11.1" +version = "7.15.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:057c0aedcade895c0d25c06daff00fb381dea8089434ec916e59b051e5dead68"}, - {file = "coverage-7.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea73d4b5a489ea60ebce592ea516089d2bee8b299fb465fdd295264da98b2480"}, - {file = "coverage-7.11.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63f837e043f7f0788c2ce8fc6bbbcc3579f123af9cb284e1334099969222ceab"}, - {file = "coverage-7.11.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:086764f9fa6f4fa57035ed1c2387501c57092f2159bf1be0f090f85f9042ccf2"}, - {file = "coverage-7.11.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a30a6ba3b668227d5a6f9f6ac2d875117af20f260ddc01619487174036a5583"}, - {file = "coverage-7.11.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2663b19df42932a2cd66e62783f4bbbca047853ede893d48f3271c5e12c89246"}, - {file = "coverage-7.11.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8c6570122b2eafaa5f4b54700b6f17ee10e23c5cf4292fa9b5a00e9dc279a74"}, - {file = "coverage-7.11.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2cf57b5be59d36d133c06103f50c72bfdba7c7624d68b443b16a2d2d4eb40424"}, - {file = "coverage-7.11.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f3f3eb204cbe221ef9209e34341b3d0bc32f4cf3c7c4f150db571e20b9963ecd"}, - {file = "coverage-7.11.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:57d36cb40ad55fe443bb2390c759c61b9fa3afc68d5446a2aaed1ad18fc92752"}, - {file = "coverage-7.11.1-cp310-cp310-win32.whl", hash = "sha256:999a82a2dec9e31df7cb49a17e6b564b76fab3f9cd76788280997b5a694b8025"}, - {file = "coverage-7.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:d47ad0fdc96d5772fcded1a57f042a72dba893a226d3efa5802d0bfa88e3a9a1"}, - {file = "coverage-7.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f8be6327cb57e73f1933a111b31ca3e8db68eba70921244296cd9541f8405cf"}, - {file = "coverage-7.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3386b3d974eea5b8fbc31388c2847d5b3ce783aa001048c7c13ad0e0f9f97284"}, - {file = "coverage-7.11.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd5a0e53989aa0d2b94871ac9a990f7b6247c3afe49af77f8750d7bcf1e66efa"}, - {file = "coverage-7.11.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e17d99e4a9989ccc52d672543ed9d8741d90730ba331d452793be5733b4fee58"}, - {file = "coverage-7.11.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ece0ace8d8fc20cc29e2108d4031517c03d9e08883f10c1df16bef84d469110"}, - {file = "coverage-7.11.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54bf4a13bfcf6f07c4b7d83970074dc2fa8b5782e8dee962f5eb4dfbc3a275ef"}, - {file = "coverage-7.11.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b48e85160795648323fc3a9d8efe11be65a033b564e1db28b53866810da6cf35"}, - {file = "coverage-7.11.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b77e7bb5765988a7a80463b999085cd66c6515113fc88b46910217f19ee99fe"}, - {file = "coverage-7.11.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ce345819ddedcbe797d8ba824deeb0d55710037dfd47efd95709ab9e1b841e0c"}, - {file = "coverage-7.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:abde2bd52560527124d9e6515daa1f1e3c7e820a37af63d063723867775220aa"}, - {file = "coverage-7.11.1-cp311-cp311-win32.whl", hash = "sha256:049883a469ec823b1c9556050380e61f580d52f8abfc8be2071f3512a2bc3859"}, - {file = "coverage-7.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:784a9fe33335296857db05b97dcb16df811418515a2355fc4811b0c2b029b4be"}, - {file = "coverage-7.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:2bcfeb983a53f0d3ee3ebc004827723d8accb619f64bf90aff73b7703dfe14bd"}, - {file = "coverage-7.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:421e2d237dcecdefa9b77cae1aa0dfff5c495f29e053e776172457e289976311"}, - {file = "coverage-7.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:08ef89c812072ecd52a862b46e131f75596475d23cc7f5a75410394341d4332f"}, - {file = "coverage-7.11.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bc6e0b2d6ed317810b4e435ffabc31b2d517d6ceb4183dfd6af4748c52d170eb"}, - {file = "coverage-7.11.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b59736704df8b1f8b1dafb36b16f2ef8a952e4410465634442459426bd2319ae"}, - {file = "coverage-7.11.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:843816452d8bfc4c2be72546b3b382850cb91150feaa963ec7d2b665ec9d4768"}, - {file = "coverage-7.11.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:19363046125d4a423c25d3d7c90bab3a0230932c16014198f87a6b3960c1b187"}, - {file = "coverage-7.11.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e37486aed7045c280ebdc207026bdef9267730177d929a5e25250e1f33cc125"}, - {file = "coverage-7.11.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7c68180e67b4843674bfb1d3ec928ffcfc94081b5da959e616405eca51c23356"}, - {file = "coverage-7.11.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cf825b60f94d1706c22d4887310db26cc3117d545ac6ad4229b4a0d718afcf9a"}, - {file = "coverage-7.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:437149272ff0440df66044bd6ee87cbc252463754ca43cafa496cfb2f57f56dd"}, - {file = "coverage-7.11.1-cp312-cp312-win32.whl", hash = "sha256:98ea0b8d1addfc333494c2248af367e8ecb27724a99804a18376b801f876da58"}, - {file = "coverage-7.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d49a473799e55a465bcadd19525977ab80031b8b86baaa622241808df4585cd"}, - {file = "coverage-7.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:0c77e5951ab176a6ccb70c6f688fca2a7ac834753ba82ee4eb741be655f30b43"}, - {file = "coverage-7.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:da9930594ca99d66eb6f613d7beba850db2f8dfa86810ee35ae24e4d5f2bb97d"}, - {file = "coverage-7.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc47a280dc014220b0fc6e5f55082a3f51854faf08fd9635b8a4f341c46c77d3"}, - {file = "coverage-7.11.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:74003324321bbf130939146886eddf92e48e616b5910215e79dea6edeb8ee7c8"}, - {file = "coverage-7.11.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:211f7996265daab60a8249af4ca6641b3080769cbedcffc42cc4841118f3a305"}, - {file = "coverage-7.11.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70619d194d8fea0cb028cb6bb9c85b519c7509c1d1feef1eea635183bc8ecd27"}, - {file = "coverage-7.11.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0208bb59d441cfa3321569040f8e455f9261256e0df776c5462a1e5a9b31e13"}, - {file = "coverage-7.11.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:545714d8765bda1c51f8b1c96e0b497886a054471c68211e76ef49dd1468587d"}, - {file = "coverage-7.11.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d0a2b02c1e20158dd405054bcca87f91fd5b7605626aee87150819ea616edd67"}, - {file = "coverage-7.11.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0f4aa986a4308a458e0fb572faa3eb3db2ea7ce294604064b25ab32b435a468"}, - {file = "coverage-7.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d51cc6687e8bbfd1e041f52baed0f979cd592242cf50bf18399a7e03afc82d88"}, - {file = "coverage-7.11.1-cp313-cp313-win32.whl", hash = "sha256:1b3067db3afe6deeca2b2c9f0ec23820d5f1bd152827acfadf24de145dfc5f66"}, - {file = "coverage-7.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:39a4c44b0cd40e3c9d89b2b7303ebd6ab9ae8a63f9e9a8c4d65a181a0b33aebe"}, - {file = "coverage-7.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:a2e3560bf82fa8169a577e054cbbc29888699526063fee26ea59ea2627fd6e73"}, - {file = "coverage-7.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47a4f362a10285897ab3aa7a4b37d28213a4f2626823923613d6d7a3584dd79a"}, - {file = "coverage-7.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0df35fa7419ef571db9dacd50b0517bc54dbfe37eb94043b5fc3540bff276acd"}, - {file = "coverage-7.11.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e1a2c621d341c9d56f7917e56fbb56be4f73fe0d0e8dae28352fb095060fd467"}, - {file = "coverage-7.11.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c354b111be9b2234d9573d75dd30ca4e414b7659c730e477e89be4f620b3fb5"}, - {file = "coverage-7.11.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4589bd44698728f600233fb2881014c9b8ec86637ef454c00939e779661dbe7e"}, - {file = "coverage-7.11.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c6956fc8754f2309131230272a7213a483a32ecbe29e2b9316d808a28f2f8ea1"}, - {file = "coverage-7.11.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63926a97ed89dc6a087369b92dcb8b9a94cead46c08b33a7f1f4818cd8b6a3c3"}, - {file = "coverage-7.11.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f5311ba00c53a7fb2b293fdc1f478b7286fe2a845a7ba9cda053f6e98178f0b4"}, - {file = "coverage-7.11.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:31bf5ffad84c974f9e72ac53493350f36b6fa396109159ec704210698f12860b"}, - {file = "coverage-7.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:227ee59fbc4a8c57a7383a1d7af6ca94a78ae3beee4045f38684548a8479a65b"}, - {file = "coverage-7.11.1-cp313-cp313t-win32.whl", hash = "sha256:a447d97b3ce680bb1da2e6bd822ebb71be6a1fb77ce2c2ad2fe4bd8aacec3058"}, - {file = "coverage-7.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6d11180437c67bde2248563a42b8e5bbf85c8df78fae13bf818ad17bfb15f02"}, - {file = "coverage-7.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:1e19a4c43d612760c6f7190411fb157e2d8a6dde00c91b941d43203bd3b17f6f"}, - {file = "coverage-7.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0305463c45c5f21f0396cd5028de92b1f1387e2e0756a85dd3147daa49f7a674"}, - {file = "coverage-7.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fa4d468d5efa1eb6e3062be8bd5f45cbf28257a37b71b969a8c1da2652dfec77"}, - {file = "coverage-7.11.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d2b2f5fc8fe383cbf2d5c77d6c4b2632ede553bc0afd0cdc910fa5390046c290"}, - {file = "coverage-7.11.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bde6488c1ad509f4fb1a4f9960fd003d5a94adef61e226246f9699befbab3276"}, - {file = "coverage-7.11.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a69e0d6fa0b920fe6706a898c52955ec5bcfa7e45868215159f45fd87ea6da7c"}, - {file = "coverage-7.11.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:976e51e4a549b80e4639eda3a53e95013a14ff6ad69bb58ed604d34deb0e774c"}, - {file = "coverage-7.11.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d61fcc4d384c82971a3d9cf00d0872881f9ded19404c714d6079b7a4547e2955"}, - {file = "coverage-7.11.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:284c5df762b533fae3ebd764e3b81c20c1c9648d93ef34469759cb4e3dfe13d0"}, - {file = "coverage-7.11.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:bab32cb1d4ad2ac6dcc4e17eee5fa136c2a1d14ae914e4bce6c8b78273aece3c"}, - {file = "coverage-7.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:36f2fed9ce392ca450fb4e283900d0b41f05c8c5db674d200f471498be3ce747"}, - {file = "coverage-7.11.1-cp314-cp314-win32.whl", hash = "sha256:853136cecb92a5ba1cc8f61ec6ffa62ca3c88b4b386a6c835f8b833924f9a8c5"}, - {file = "coverage-7.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:77443d39143e20927259a61da0c95d55ffc31cf43086b8f0f11a92da5260d592"}, - {file = "coverage-7.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:829acb88fa47591a64bf5197e96a931ce9d4b3634c7f81a224ba3319623cdf6c"}, - {file = "coverage-7.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2ad1fe321d9522ea14399de83e75a11fb6a8887930c3679feb383301c28070d9"}, - {file = "coverage-7.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f69c332f0c3d1357c74decc9b1843fcd428cf9221bf196a20ad22aa1db3e1b6c"}, - {file = "coverage-7.11.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:576baeea4eebde684bf6c91c01e97171c8015765c8b2cfd4022a42b899897811"}, - {file = "coverage-7.11.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:28ad84c694fa86084cfd3c1eab4149844b8cb95bd8e5cbfc4a647f3ee2cce2b3"}, - {file = "coverage-7.11.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1043ff958f09fc3f552c014d599f3c6b7088ba97d7bc1bd1cce8603cd75b520"}, - {file = "coverage-7.11.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c6681add5060c2742dafcf29826dff1ff8eef889a3b03390daeed84361c428bd"}, - {file = "coverage-7.11.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:773419b225ec9a75caa1e941dd0c83a91b92c2b525269e44e6ee3e4c630607db"}, - {file = "coverage-7.11.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a9cb272a0e0157dbb9b2fd0b201b759bd378a1a6138a16536c025c2ce4f7643b"}, - {file = "coverage-7.11.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e09adb2a7811dc75998eef68f47599cf699e2b62eed09c9fefaeb290b3920f34"}, - {file = "coverage-7.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1335fa8c2a2fea49924d97e1e3500cfe8d7c849f5369f26bb7559ad4259ccfab"}, - {file = "coverage-7.11.1-cp314-cp314t-win32.whl", hash = "sha256:4782d71d2a4fa7cef95e853b7097c8bbead4dbd0e6f9c7152a6b11a194b794db"}, - {file = "coverage-7.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:939f45e66eceb63c75e8eb8fc58bb7077c00f1a41b0e15c6ef02334a933cfe93"}, - {file = "coverage-7.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:01c575bdbef35e3f023b50a146e9a75c53816e4f2569109458155cd2315f87d9"}, - {file = "coverage-7.11.1-py3-none-any.whl", hash = "sha256:0fa848acb5f1da24765cee840e1afe9232ac98a8f9431c6112c15b34e880b9e8"}, - {file = "coverage-7.11.1.tar.gz", hash = "sha256:b4b3a072559578129a9e863082a2972a2abd8975bc0e2ec57da96afcd6580a8a"}, + {file = "coverage-7.15.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0be6daac4cce6b8c8dc65886bae1b082ddbca4da8e5cbb5e15166acf253e264"}, + {file = "coverage-7.15.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b24e078eabcd6a9caa8b0713f9bc1eeb310bcc960a29d45a3b4fcd4b16d5b11d"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe20cc8cf8821d4fe54f89106cbf06aa27f37b5bbe3535568065a81539b4150"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83cf06cdd687677742caff1a9134833b7a8b75f111519d2cb0e0ba1b9a851e15"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fa4de68e2a752468ff14b4e15db7def689a71be759e826a31ccecbef69c5fd0"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4dff9daa47d83120c3ec38ce921214242944a832aa04e903e50b5b7ebac8972d"}, + {file = "coverage-7.15.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a093fd37229918976f602aa07aa59e0973cde82186f220c8e197f721f5be0ce4"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:317db01a2cb02552fd67e2b1cca77a4b528a2a277176c5e0bf2cecbb639d3f54"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8ee3838dcb656602c3b51e16aed9bfb0822f8d8d6d1c5966d32ec8c104be8e20"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:425920379052ff1fe465268f3361d35804a241bbdd5a1b592c8cb60df4c52325"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:69bb2400abef928e365ea7d4d9925169ada78ed2295546780002d4b65de3df88"}, + {file = "coverage-7.15.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:81661f82d302484e3119e7c80c519c02fa9bcc2a6b339baf67d67bc89c580f04"}, + {file = "coverage-7.15.4-cp310-cp310-win32.whl", hash = "sha256:cb476b2e828ecb71cb6b6a928d23fd20a7ddb501188022dae1c37499149cc338"}, + {file = "coverage-7.15.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fc2130bf37df31852a8384f12601563a45a0024bccc6624f38355cba7a8d360"}, + {file = "coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490"}, + {file = "coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce"}, + {file = "coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719"}, + {file = "coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7"}, + {file = "coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e"}, + {file = "coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc"}, + {file = "coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890"}, + {file = "coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22"}, + {file = "coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8"}, + {file = "coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c"}, + {file = "coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4"}, + {file = "coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b"}, + {file = "coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd"}, + {file = "coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f"}, + {file = "coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921"}, + {file = "coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7"}, + {file = "coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d"}, + {file = "coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff"}, + {file = "coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c"}, + {file = "coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4"}, + {file = "coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf"}, + {file = "coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f"}, + {file = "coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d"}, + {file = "coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f"}, + {file = "coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5"}, + {file = "coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7"}, + {file = "coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425"}, + {file = "coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8"}, + {file = "coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8"}, + {file = "coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982"}, + {file = "coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017"}, + {file = "coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839"}, + {file = "coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85"}, + {file = "coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e"}, + {file = "coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e"}, + {file = "coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9"}, + {file = "coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce"}, + {file = "coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768"}, + {file = "coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753"}, + {file = "coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2"}, + {file = "coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809"}, + {file = "coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d"}, + {file = "coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d"}, + {file = "coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa"}, + {file = "coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88"}, + {file = "coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc"}, + {file = "coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a"}, + {file = "coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b"}, + {file = "coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278"}, + {file = "coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84"}, + {file = "coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00"}, ] [package.extras] @@ -724,58 +787,58 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "49.0.0" +version = "50.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.9" groups = ["main"] files = [ - {file = "cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68"}, - {file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9"}, - {file = "cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f"}, - {file = "cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459"}, - {file = "cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e"}, - {file = "cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866"}, - {file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8"}, - {file = "cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3"}, - {file = "cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27"}, - {file = "cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61"}, - {file = "cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8"}, - {file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36"}, - {file = "cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e"}, - {file = "cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b"}, - {file = "cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6"}, - {file = "cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6"}, - {file = "cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493"}, + {file = "cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f"}, + {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105"}, + {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef"}, + {file = "cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30"}, + {file = "cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c"}, + {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c"}, + {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95"}, + {file = "cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269"}, + {file = "cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47"}, + {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9"}, + {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7"}, + {file = "cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9"}, + {file = "cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9"}, ] [package.dependencies] @@ -806,28 +869,28 @@ dev = ["flake8", "hypothesis", "ipython", "mypy (>=0.710)", "portray", "pytest ( [[package]] name = "deptry" -version = "0.23.1" +version = "0.25.1" description = "A command line utility to check for unused, missing and transitive dependencies in a Python project." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "deptry-0.23.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f0b231d098fb5b48d8973c9f192c353ffdd395770063424969fa7f15ddfea7d8"}, - {file = "deptry-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf057f514bb2fa18a2b192a7f7372bd14577ff46b11486933e8383dfef461983"}, - {file = "deptry-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ee3f5663bb1c048e2aaf25a4d9e6d09cc1f3b3396ee248980878c6a6c9c0e21"}, - {file = "deptry-0.23.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae0366dc5f50a5fb29cf90de1110c5e368513de6c1b2dac439f2817f3f752616"}, - {file = "deptry-0.23.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ab156a90a9eda5819aeb1c1da585dd4d5ec509029399a38771a49e78f40db90f"}, - {file = "deptry-0.23.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:651c7eb168233755152fcc468713c024d64a03069645187edb4a17ba61ce6133"}, - {file = "deptry-0.23.1-cp39-abi3-win_amd64.whl", hash = "sha256:8da1e8f70e7086ebc228f3a4a3cfb5aa127b09b5eef60d694503d6bb79809025"}, - {file = "deptry-0.23.1-cp39-abi3-win_arm64.whl", hash = "sha256:f589497a5809717db4dcf2aa840f2847c0a4c489331608e538850b6a9ab1c30b"}, - {file = "deptry-0.23.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6af91d86380ef703adb6ae65f273d88e3cca7fd315c4c309da857a0cfa728244"}, - {file = "deptry-0.23.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:42a249d317c3128c286035a1f7aaa41a0c3c967f17848817c2e07ca50d5ed450"}, - {file = "deptry-0.23.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d988c7c75201997970bae1e8d564b4c7a14d350556c4f7c269fd33f3b081c314"}, - {file = "deptry-0.23.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae13d8e65ae88b77632c45edb4038301a6f9efcac06715abfde9a029e5879698"}, - {file = "deptry-0.23.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:40058a7a3fe9dacb745668897ee992e58daf5aac406b668ff2eaaf0f6f586550"}, - {file = "deptry-0.23.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d111cf4261eeadbdb20051d8d542f04deb3cfced0cb280ece8d654f7f6055921"}, - {file = "deptry-0.23.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9f9bbb92f95ada9ccfa5ecefee05ba3c39cfa0734b5483a3a1a3c4eeb9c99054"}, - {file = "deptry-0.23.1.tar.gz", hash = "sha256:5d23e0ef25f3c56405c05383a476edda55944563c5c47a3e9249ed3ec860d382"}, + {file = "deptry-0.25.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a4dd1148db24a1ddacfa8b840836c6019c2f864fcb7579dd089fd217606338c8"}, + {file = "deptry-0.25.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c67c666d916ef12013c0772e40d78be0f21577a495d8d99ec5fcb18c332d393d"}, + {file = "deptry-0.25.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58d39279828dbf4efc1abb40bf50a71b21499c36759bed5a8d8a3c0e3149b091"}, + {file = "deptry-0.25.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14bfcc28b4326ed8c6abb30691b19077d4ef8613cfba6c37ef5b1f471775bf6f"}, + {file = "deptry-0.25.1-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:555f5f9a487899ec9bf301eecba1745e14d212c4b354f4d3a5fd691e907366d3"}, + {file = "deptry-0.25.1-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d21b3545ab2bfec53f3f45c6f5f201d55f713323327f8d12674505469ae6b7"}, + {file = "deptry-0.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:b59a560cb7dffb21832a98bb80d33d614cfb5630ea36ce21833eabf4eae3df99"}, + {file = "deptry-0.25.1-cp310-abi3-win_arm64.whl", hash = "sha256:6efffd8116fb9d2c45a251382ce4ce1c38dbb17179f581ec9231ed5390f7fc12"}, + {file = "deptry-0.25.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:30d64d4df1c08bc69de56cb0b4ec1f4cd9fa2e42582347d5b1eb25fd0e401745"}, + {file = "deptry-0.25.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:87bcd90f99a98bb059c7580bc315c3f87d97fe2db725530030bc974176834735"}, + {file = "deptry-0.25.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f31eb5c520651b102568dd91f738222b250a3e44c9e95d4941322109b8d40a"}, + {file = "deptry-0.25.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df88952a2bab7517ef23cb304b979199b28449e5d9db2e9ba9bc27a286ac852b"}, + {file = "deptry-0.25.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e6f7b8fa72932e51e86799b10dcd29381b2132dc799c790dca3b28ab08dffb28"}, + {file = "deptry-0.25.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e3fa3321078e11cd1ac3f10ce3ff0547731c53f9253b87c757a8749c76fe8fa9"}, + {file = "deptry-0.25.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:03c032c32492fde434736954fbcaff09c02bf207b0f793b77e9040300e34b344"}, + {file = "deptry-0.25.1.tar.gz", hash = "sha256:45c8cd982c85cd4faae573ddff6920de7eec735336db6973f26a765ae7950f7d"}, ] [package.dependencies] @@ -835,117 +898,136 @@ click = ">=8.0.0,<9" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} packaging = ">=23.2" requirements-parser = ">=0.11.0,<1" +tomli = {version = ">=2.0.1", markers = "python_full_version < \"3.15.0\""} [[package]] name = "dictionaryutils" -version = "3.5.1" +version = "4.0.0" description = "Python wrapper and metaschema for datadictionary." optional = false -python-versions = "<4,>=3.9" +python-versions = "<4,>=3.13" groups = ["main"] files = [ - {file = "dictionaryutils-3.5.1-py3-none-any.whl", hash = "sha256:a6d9b5cd81e3b29a087cb5deca8fef788a46a1e576a82b240bafd78cba37e8fb"}, - {file = "dictionaryutils-3.5.1.tar.gz", hash = "sha256:c63a47479f671881f0f00aa7f647751239f02cbc930536246a30d349bc1ea34b"}, + {file = "dictionaryutils-4.0.0-py3-none-any.whl", hash = "sha256:a6d1b2af957ffd2c66df42a1398fee1bd30b0268df82d9630236ed23286500a0"}, + {file = "dictionaryutils-4.0.0.tar.gz", hash = "sha256:c846c48d534c7ae4a47d65b29cb7bb4324e998366e81ff5cc2175c94b1ea0c77"}, ] [package.dependencies] cdislogging = "*" -gen3dictionary = ">=2.0.4,<3.0.0" +gen3dictionary = ">=3.0.0" jsonschema = "<=4.23.0" PyYAML = "*" requests = "*" [[package]] name = "drsclient" -version = "0.3.2" +version = "0.4.0" description = "GA4GH DRS Client" optional = false -python-versions = "<4.0,>=3.9" +python-versions = "<4,>=3.13" groups = ["main"] files = [ - {file = "drsclient-0.3.2-py3-none-any.whl", hash = "sha256:e4ba8fe9dc46749ffe6e70956282a0b159e61c5829a134806db8cdf78769c001"}, - {file = "drsclient-0.3.2.tar.gz", hash = "sha256:a3e97362d7dd2fc64ffbf265a11d99dafad7b7e5ef9d0e1fcab8f97f555eb662"}, + {file = "drsclient-0.4.0-py3-none-any.whl", hash = "sha256:34e5f18b060894cf1402929a84ef96f1c3a80ad07447443dbb575a9324366dff"}, + {file = "drsclient-0.4.0.tar.gz", hash = "sha256:b8fbffbe23fe4e9722f9c511821b7d0c79c8a1ada5a54557e1a0a138894a9422"}, ] [package.dependencies] -asyncio = ">=3.4.3,<4.0.0" -backoff = ">=1.10.0,<2.0.0" httpx = ">=0.28.1,<1.0.0" requests = ">=2.23.0,<3.0.0" +[[package]] +name = "execnet" +version = "2.1.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, + {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "fastavro" -version = "1.12.1" +version = "1.12.2" description = "Fast read/write of AVRO files" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "fastavro-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:00650ca533907361edda22e6ffe8cf87ab2091c5d8aee5c8000b0f2dcdda7ed3"}, - {file = "fastavro-1.12.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac76d6d95f909c72ee70d314b460b7e711d928845771531d823eb96a10952d26"}, - {file = "fastavro-1.12.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55eef18c41d4476bd32a82ed5dd86aabc3f614e1b66bdb09ffa291612e1670"}, - {file = "fastavro-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81563e1f93570e6565487cdb01ba241a36a00e58cff9c5a0614af819d1155d8f"}, - {file = "fastavro-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec207360f76f0b3de540758a297193c5390e8e081c43c3317f610b1414d8c8f"}, - {file = "fastavro-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:c0390bfe4a9f8056a75ac6785fbbff8f5e317f5356481d2e29ec980877d2314b"}, - {file = "fastavro-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b632b713bc5d03928a87d811fa4a11d5f25cd43e79c161e291c7d3f7aa740fd"}, - {file = "fastavro-1.12.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa7ab3769beadcebb60f0539054c7755f63bd9cf7666e2c15e615ab605f89a8"}, - {file = "fastavro-1.12.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123fb221df3164abd93f2d042c82f538a1d5a43ce41375f12c91ce1355a9141e"}, - {file = "fastavro-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:632a4e3ff223f834ddb746baae0cc7cee1068eb12c32e4d982c2fee8a5b483d0"}, - {file = "fastavro-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e6caf4e7a8717d932a3b1ff31595ad169289bbe1128a216be070d3a8391671"}, - {file = "fastavro-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:b91a0fe5a173679a6c02d53ca22dcaad0a2c726b74507e0c1c2e71a7c3f79ef9"}, - {file = "fastavro-1.12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:509818cb24b98a804fc80be9c5fed90f660310ae3d59382fc811bfa187122167"}, - {file = "fastavro-1.12.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089e155c0c76e0d418d7e79144ce000524dd345eab3bc1e9c5ae69d500f71b14"}, - {file = "fastavro-1.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44cbff7518901c91a82aab476fcab13d102e4999499df219d481b9e15f61af34"}, - {file = "fastavro-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a275e48df0b1701bb764b18a8a21900b24cf882263cb03d35ecdba636bbc830b"}, - {file = "fastavro-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2de72d786eb38be6b16d556b27232b1bf1b2797ea09599507938cdb7a9fe3e7c"}, - {file = "fastavro-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:9090f0dee63fe022ee9cc5147483366cc4171c821644c22da020d6b48f576b4f"}, - {file = "fastavro-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78df838351e4dff9edd10a1c41d1324131ffecbadefb9c297d612ef5363c049a"}, - {file = "fastavro-1.12.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780476c23175d2ae457c52f45b9ffa9d504593499a36cd3c1929662bf5b7b14b"}, - {file = "fastavro-1.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0714b285160fcd515eb0455540f40dd6dac93bdeacdb03f24e8eac3d8aa51f8d"}, - {file = "fastavro-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc2dcec5843d499f2489bfe0747999108f78c5b29295d877379f1972a3d41a"}, - {file = "fastavro-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b1921ac35f3d89090a5816b626cf46e67dbecf3f054131f84d56b4e70496f45"}, - {file = "fastavro-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:5aa777b8ee595b50aa084104cd70670bf25a7bbb9fd8bb5d07524b0785ee1699"}, - {file = "fastavro-1.12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c3d67c47f177e486640404a56f2f50b165fe892cc343ac3a34673b80cc7f1dd6"}, - {file = "fastavro-1.12.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5217f773492bac43dae15ff2931432bce2d7a80be7039685a78d3fab7df910bd"}, - {file = "fastavro-1.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:469fecb25cba07f2e1bfa4c8d008477cd6b5b34a59d48715e1b1a73f6160097d"}, - {file = "fastavro-1.12.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d71c8aa841ef65cfab709a22bb887955f42934bced3ddb571e98fdbdade4c609"}, - {file = "fastavro-1.12.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b81fc04e85dfccf7c028e0580c606e33aa8472370b767ef058aae2c674a90746"}, - {file = "fastavro-1.12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9445da127751ba65975d8e4bdabf36bfcfdad70fc35b2d988e3950cce0ec0e7c"}, - {file = "fastavro-1.12.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed924233272719b5d5a6a0b4d80ef3345fc7e84fc7a382b6232192a9112d38a6"}, - {file = "fastavro-1.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3616e2f0e1c9265e92954fa099db79c6e7817356d3ff34f4bcc92699ae99697c"}, - {file = "fastavro-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb0337b42fd3c047fcf0e9b7597bd6ad25868de719f29da81eabb6343f08d399"}, - {file = "fastavro-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:64961ab15b74b7c168717bbece5660e0f3d457837c3cc9d9145181d011199fa7"}, - {file = "fastavro-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:792356d320f6e757e89f7ac9c22f481e546c886454a6709247f43c0dd7058004"}, - {file = "fastavro-1.12.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120aaf82ac19d60a1016afe410935fe94728752d9c2d684e267e5b7f0e70f6d9"}, - {file = "fastavro-1.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6a3462934b20a74f9ece1daa49c2e4e749bd9a35fa2657b53bf62898fba80f5"}, - {file = "fastavro-1.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f81011d54dd47b12437b51dd93a70a9aa17b61307abf26542fc3c13efbc6c51"}, - {file = "fastavro-1.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43ded16b3f4a9f1a42f5970c2aa618acb23ea59c4fcaa06680bdf470b255e5a8"}, - {file = "fastavro-1.12.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:02281432dcb11c78b3280da996eff61ee0eff39c5de06c6e0fbf19275093e6d4"}, - {file = "fastavro-1.12.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4128978b930aaf930332db4b3acc290783183f3be06a241ae4a482f3ed8ce892"}, - {file = "fastavro-1.12.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:546ffffda6610fca672f0ed41149808e106d8272bb246aa7539fa8bb6f117f17"}, - {file = "fastavro-1.12.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a7d840ccd9aacada3ddc80fbcc4ea079b658107fe62e9d289a0de9d54e95d366"}, - {file = "fastavro-1.12.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3100ad643e7fa658469a2a2db229981c1a000ff16b8037c0b58ce3ec4d2107e8"}, - {file = "fastavro-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:a38607444281619eda3a9c1be9f5397634012d1b237142eee1540e810b30ac8b"}, - {file = "fastavro-1.12.1.tar.gz", hash = "sha256:2f285be49e45bc047ab2f6bed040bb349da85db3f3c87880e4b92595ea093b2b"}, + {file = "fastavro-1.12.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7c6d26c731a0e1e8e7d4ae8f13ae524eb6ec0e90d99c8147a19fdbae14eb807"}, + {file = "fastavro-1.12.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7caeecf519eff50f007ca4bee16b6e0a8252e5fe682c94432192a20867239888"}, + {file = "fastavro-1.12.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:731aefe6c4bf2bafa0798ef83927676d06e44d1d18202cfb56d63b40422ab900"}, + {file = "fastavro-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f089f24225a28ddafa5cfad7c41cfa84db1a55f2d473370769a95c0e3bac60c9"}, + {file = "fastavro-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:653c4f90dd21d8a1e74309919e08934e420d9aef51d051d14bf5a1c0e8293c22"}, + {file = "fastavro-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:030f17eb4c7978538a31b55dea451ceace851a88dc9816b1923f8fb8a260db4c"}, + {file = "fastavro-1.12.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d48cd7094598a7e9d4297e8bf4bbe0dc9dc2ba4367d83dbb603e3b3c6aa35566"}, + {file = "fastavro-1.12.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:070c6134604bd7b6fd44409406ac50445339682b2e872885db2e859f92d22e93"}, + {file = "fastavro-1.12.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b73d50978d5e57416fa68461f9f3c8f39ea39e761cb1e12f919745adefe26a7"}, + {file = "fastavro-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c57a9920400166398695d92580eca21fd7a79f3c67d691ac7e20a7d1b5300735"}, + {file = "fastavro-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:81f6108f3ac292fb6cd05758c9e531389d8fc5e94e8c949b9298f4fb0a239662"}, + {file = "fastavro-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:eec44256856fd59d29d1f1d0950ace18a58e4228e7d49de5d5e1b1875b227dde"}, + {file = "fastavro-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:ecd1b23ea7f9af09c865ac8503d07afd7e6bf782d76bb83cbbdba15b7a0db807"}, + {file = "fastavro-1.12.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e331896e8efffc72fa03e63b87ebfc37960113127da8e0f5152d91664ffed68"}, + {file = "fastavro-1.12.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f01ebaada59d74fdf6d28e5031a961a413b3752e9edb0c03866fa18480cf4c8"}, + {file = "fastavro-1.12.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25ef6855935f67582740ffa6bb978e40ec51be876117a3555c36fa2488dcdf25"}, + {file = "fastavro-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84a4f76a0aece0aa72b5ed8162ba2ff8c78908b8361b5a5d92ddd161977ccb74"}, + {file = "fastavro-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e8da77d201916f6771fc357fda8267c2a256d7aa11923d43bc5f2fc155878b"}, + {file = "fastavro-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:1924349c74666c89417bd5cc2749f598e2f15f1d56ee81428b2317ab02c88aae"}, + {file = "fastavro-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:4c346cf449baf3b113e997c34151ad205e7135bc429469b005b180ade7e65e28"}, + {file = "fastavro-1.12.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:57bb6b908cb2e05baab63b04c3a31be3b4545a10bfab9748b8763016b5256704"}, + {file = "fastavro-1.12.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a007f95cc682f56e6d83f1d17c29c00bf719d6fe8e003282b535af3a1ba09c0"}, + {file = "fastavro-1.12.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e90460b0cd21f62be3cb26087e706e2cebb7b3fcef9e05b4473b61bb0415b5e"}, + {file = "fastavro-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ccd15966b8218d41b06ec3e7c2556be89a8a693026c771e6564d2e40bbaf8ea"}, + {file = "fastavro-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06b6971d3dae10cb34353b857d16ad21ebd6f0ea394e86c96abdcad109005d6e"}, + {file = "fastavro-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:98dfcdfaf1498ae2f0e2fafe900a82e8320cc81d8ae5a95b8b8879eaa3298c39"}, + {file = "fastavro-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:3888ef7a51adc77cdf07251bc762566a1be36211e1cff689f13980f3776a2f36"}, + {file = "fastavro-1.12.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:283dcd3129b632021894425974bedd0eb6db3bbf5994e448ccad10db4d803d31"}, + {file = "fastavro-1.12.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d125e210d5a0a1f701f12c0ecad9a03f1b04b5eddbce6ca36a1fc217da977ef"}, + {file = "fastavro-1.12.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2d4d66afad78e8f47feaa307728a6b71fe3effc63ba2b9eeb109ee687c9bd397"}, + {file = "fastavro-1.12.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2328ec07925c04c89719e3971c9068a165c7fd474ea87675b1204de0440e71ff"}, + {file = "fastavro-1.12.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:55dea7e74b834d4b70467fc19c5b9ccb5509fe39abc4d26891187c1b22176423"}, + {file = "fastavro-1.12.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8d37c87826ae7195cfbd20fcd448801f2f563bb38f2691ec6574e39cb9eca6c8"}, + {file = "fastavro-1.12.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c463a3701f293e30d3d62e71e1989f112028d07f87432baf4507eeb57ec3831"}, + {file = "fastavro-1.12.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f604ba83498e209fff4c7ecc5063a39421dc538dace694bc592f9f338254f3dc"}, + {file = "fastavro-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bfac2dada8ddc002e8b7d8289d6fad4f070bc1fec20371cec684a7d10d932e96"}, + {file = "fastavro-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bc44ba6289fb1f5ee318335958dde6ad6d742dcb4bb8930de843e9024c64b68c"}, + {file = "fastavro-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:a475418f71c5aed69899813ecccf392429c08c3a63df3030129db71760b0db8f"}, + {file = "fastavro-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:daec9f9655a1d4636613c47d6d3343f6e039150d66cdce62543e20ca36612a8a"}, + {file = "fastavro-1.12.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:57594b72cf663bbd0f3ad8a319a999fc3d7c71065a6799b2c1d1a6a137894c5b"}, + {file = "fastavro-1.12.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74412132bbfb153cbf704517f2c89f7d3e170feb681b13bceace690f66f8d5fa"}, + {file = "fastavro-1.12.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e367a84c9133018e0a3bc822abe78d7f1f9a6092991a0ec409468cf4ef260282"}, + {file = "fastavro-1.12.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:044fafca0853e9ae14009de7763ac9e8e8f8b96f8a4e90bd58b695443266a370"}, + {file = "fastavro-1.12.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afede7324822800e4f90e96b9514188a237a60f35e8e7a10b2129c10c78f6e4d"}, + {file = "fastavro-1.12.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b5539711dfa1ec8f3eca57482b93a48a165af4a99e9d5f41e3af3fb913aadf92"}, + {file = "fastavro-1.12.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c01b0f0ce030a7b89263c0236ca77923eae352c5f35ecf214b04d3aaea8eb2c3"}, + {file = "fastavro-1.12.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:742d93f2ca835e4fa83a3ae9ed2bce8b28029ed62ac730f339a37685c23075cd"}, + {file = "fastavro-1.12.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ae60df21cc7059e2f3b1928ad2c0b75c6b26f9ada79d992f87b6fc3f50d3877e"}, + {file = "fastavro-1.12.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:03614131093a32c90c8fd95ff356c316752b8850cb8a770bc96cef17a003e2fc"}, + {file = "fastavro-1.12.2-cp39-cp39-win_amd64.whl", hash = "sha256:e235dfdabb51993bcd4a8f45c3a54f21a782f7f92b3def0648b0ace45a1b1ac7"}, + {file = "fastavro-1.12.2.tar.gz", hash = "sha256:3c79502d56cf6b76210032e1c53494ddfbc73c140bccf2ef4092b3f0825323ab"}, ] [package.extras] -codecs = ["cramjam", "lz4", "zstandard"] +codecs = ["backports.zstd ; python_version < \"3.14\"", "cramjam", "lz4"] lz4 = ["lz4"] snappy = ["cramjam"] -zstandard = ["zstandard"] +zstandard = ["backports.zstd ; python_version < \"3.14\""] [[package]] name = "fhirclient" -version = "4.3.2" +version = "4.4.0" description = "A flexible client for FHIR servers supporting the SMART on FHIR protocol" optional = true -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "extra == \"fhir\"" files = [ - {file = "fhirclient-4.3.2-py3-none-any.whl", hash = "sha256:7718869d4ddfd8d9d148991290276f6337b86a385dfa69ad6d1bd703074b06d2"}, - {file = "fhirclient-4.3.2.tar.gz", hash = "sha256:e71881a06167681e53d780c1a581d3e18cca3a93e789ca21acf2d8374a3c59a6"}, + {file = "fhirclient-4.4.0-py3-none-any.whl", hash = "sha256:85f25b11b2303ded7780708edc85aaf21be60fbd6439409b44085bcc1a991589"}, + {file = "fhirclient-4.4.0.tar.gz", hash = "sha256:922d6110f941741a67fdb8dc3afdeba21b8d1f30beafde7d2fbe523cf9192188"}, ] [package.dependencies] @@ -1096,13 +1178,14 @@ files = [ [[package]] name = "gen3dictionary" -version = "2.0.4" +version = "3.0.0" description = "" optional = false -python-versions = "<4.0,>=3.9" +python-versions = "<4,>=3.13" groups = ["main"] files = [ - {file = "gen3dictionary-2.0.4.tar.gz", hash = "sha256:6a798008f32c4a5c1833fbc03e841ebe12bb5b743812ec7c00211c0268f15c05"}, + {file = "gen3dictionary-3.0.0-py3-none-any.whl", hash = "sha256:5da0dcd78d7c86aaaa9b85810a4e816daa7748d66064b6f67ad96167341780ab"}, + {file = "gen3dictionary-3.0.0.tar.gz", hash = "sha256:737a3f23445a81d9c7a566da0a8ca8c5d17c3fc0eed74b9819eff74836abc3e6"}, ] [package.dependencies] @@ -1164,14 +1247,15 @@ trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpcore2" -version = "2.9.1" +version = "2.10.0" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.10" groups = ["main"] +markers = "sys_platform != \"emscripten\"" files = [ - {file = "httpcore2-2.9.1-py3-none-any.whl", hash = "sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26"}, - {file = "httpcore2-2.9.1.tar.gz", hash = "sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2"}, + {file = "httpcore2-2.10.0-py3-none-any.whl", hash = "sha256:7df06cfb34070cae4f7c89be69dc1095eca138e9704ceffb98d25c1912ab6f01"}, + {file = "httpcore2-2.10.0.tar.gz", hash = "sha256:13c0cc3d1919d4f28457f60cd2c2abe04113a8af184ccf1142811beba936f9dc"}, ] [package.dependencies] @@ -1182,7 +1266,7 @@ truststore = ">=0.10" asyncio = ["anyio (>=4.5.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] +trio = ["trio (>=0.33.0,<1.0)"] [[package]] name = "httpx" @@ -1211,30 +1295,44 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "httpx2" -version = "2.9.1" +version = "2.10.0" description = "The next generation HTTP client." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "httpx2-2.9.1-py3-none-any.whl", hash = "sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a"}, - {file = "httpx2-2.9.1.tar.gz", hash = "sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a"}, + {file = "httpx2-2.10.0-py3-none-any.whl", hash = "sha256:5e3194a432701e1cc6f69a8b1b2fa199ef907013fede8d9a09a2c5b7b8141a18"}, + {file = "httpx2-2.10.0.tar.gz", hash = "sha256:8741d7329fe2c7885fc9ceb61c8217acfb87a85f75723714b89ebf7ad7196338"}, ] [package.dependencies] -anyio = ">=4.10" -httpcore2 = "2.9.1" +anyio = {version = ">=4.10", markers = "sys_platform != \"emscripten\""} +httpcore2 = {version = "2.10.0", markers = "sys_platform != \"emscripten\""} +httpx2-jsfetch = {version = "*", markers = "sys_platform == \"emscripten\" and python_version >= \"3.12\""} idna = ">=3.18" -truststore = ">=0.10" +truststore = {version = ">=0.10", markers = "sys_platform != \"emscripten\""} [package.extras] brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] -cli = ["click (>=8.4)", "pygments (==2.*)", "rich (>=10,<16)"] +cli = ["click (>=8.4.2)", "pygments (==2.*)", "rich (>=10,<16)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] ws = ["wsproto (>=1.2)"] zstd = ["zstandard (>=0.18.0) ; python_version <= \"3.13\""] +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +description = "httpx2 transports for Emscripten/Pyodide, backed by the JavaScript fetch API." +optional = false +python-versions = ">=3.12" +groups = ["main"] +markers = "sys_platform == \"emscripten\"" +files = [ + {file = "httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32"}, + {file = "httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60"}, +] + [[package]] name = "humanfriendly" version = "10.0" @@ -1267,42 +1365,40 @@ all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "importlib-metadata" -version = "8.7.0" +version = "8.9.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, + {file = "importlib_metadata-8.9.0-py3-none-any.whl", hash = "sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f"}, + {file = "importlib_metadata-8.9.0.tar.gz", hash = "sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee"}, ] [package.dependencies] zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] +enabler = ["pytest-enabler (>=3.4)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] +test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [[package]] name = "indexclient" -version = "2.3.1" +version = "6.3.0" description = "" optional = false -python-versions = "*" +python-versions = "<4.0,>=3.13" groups = ["main"] files = [ - {file = "indexclient-2.3.1.tar.gz", hash = "sha256:0beaf865aab58112961092aa58d06e31ca1cc8da26e9cd5cf84430d2f6567a0d"}, + {file = "indexclient-6.3.0-py3-none-any.whl", hash = "sha256:77ea96da39cbd61d68703ac13cb54ca0de2305e0251ab0158b833d440e479c4a"}, + {file = "indexclient-6.3.0.tar.gz", hash = "sha256:72bc79049d8c596eafcf5e9a94d44dd9116ba56c09f36b71eda5b418d0761199"}, ] -[package.dependencies] -requests = ">=2.5.2,<3.0.0" - [[package]] name = "iniconfig" version = "2.3.0" @@ -1317,14 +1413,14 @@ files = [ [[package]] name = "joserfc" -version = "1.7.3" +version = "1.7.4" description = "The ultimate Python library for JOSE RFCs, including JWS, JWE, JWK, JWA, JWT" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "joserfc-1.7.3-py3-none-any.whl", hash = "sha256:7c39f3f2c943dbc03122747fa8ebbd8e156e54904cf25651b452f4d2634a6075"}, - {file = "joserfc-1.7.3.tar.gz", hash = "sha256:116955c2587139dba20621fd0bd7fc9255fa960c9fe7f43c43ebef2e801dcfcf"}, + {file = "joserfc-1.7.4-py3-none-any.whl", hash = "sha256:32d46c2cd5e3203c13e87a6c61333cab310b1ba80cd54b4c4f386a848a122463"}, + {file = "joserfc-1.7.4.tar.gz", hash = "sha256:b3bc561672ae541b17a9237053b48a03dacddd92d68047b3ecdfb4b5714a88ed"}, ] [package.dependencies] @@ -1456,14 +1552,14 @@ files = [ [[package]] name = "marshmallow" -version = "3.26.1" +version = "3.26.2" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"}, - {file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"}, + {file = "marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73"}, + {file = "marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57"}, ] [package.dependencies] @@ -1491,158 +1587,158 @@ marshmallow = ">=2.0.0" [[package]] name = "multidict" -version = "6.7.0" +version = "6.7.1" description = "multidict implementation" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349"}, - {file = "multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e"}, - {file = "multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3"}, - {file = "multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046"}, - {file = "multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32"}, - {file = "multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73"}, - {file = "multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc"}, - {file = "multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62"}, - {file = "multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84"}, - {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0"}, - {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e"}, - {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4"}, - {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648"}, - {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111"}, - {file = "multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36"}, - {file = "multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85"}, - {file = "multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7"}, - {file = "multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0"}, - {file = "multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc"}, - {file = "multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721"}, - {file = "multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6"}, - {file = "multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c"}, - {file = "multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7"}, - {file = "multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7"}, - {file = "multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9"}, - {file = "multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8"}, - {file = "multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd"}, - {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb"}, - {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6"}, - {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2"}, - {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff"}, - {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b"}, - {file = "multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34"}, - {file = "multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff"}, - {file = "multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81"}, - {file = "multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912"}, - {file = "multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184"}, - {file = "multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45"}, - {file = "multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa"}, - {file = "multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7"}, - {file = "multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e"}, - {file = "multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546"}, - {file = "multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4"}, - {file = "multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1"}, - {file = "multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d"}, - {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304"}, - {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12"}, - {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62"}, - {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0"}, - {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a"}, - {file = "multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8"}, - {file = "multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4"}, - {file = "multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b"}, - {file = "multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec"}, - {file = "multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6"}, - {file = "multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159"}, - {file = "multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca"}, - {file = "multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8"}, - {file = "multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60"}, - {file = "multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4"}, - {file = "multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f"}, - {file = "multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf"}, - {file = "multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32"}, - {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036"}, - {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec"}, - {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e"}, - {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64"}, - {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd"}, - {file = "multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288"}, - {file = "multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17"}, - {file = "multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390"}, - {file = "multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e"}, - {file = "multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00"}, - {file = "multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb"}, - {file = "multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b"}, - {file = "multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c"}, - {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1"}, - {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b"}, - {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5"}, - {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad"}, - {file = "multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c"}, - {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5"}, - {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10"}, - {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754"}, - {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c"}, - {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762"}, - {file = "multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6"}, - {file = "multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d"}, - {file = "multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6"}, - {file = "multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792"}, - {file = "multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842"}, - {file = "multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b"}, - {file = "multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38"}, - {file = "multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128"}, - {file = "multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34"}, - {file = "multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99"}, - {file = "multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202"}, - {file = "multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1"}, - {file = "multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3"}, - {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d"}, - {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6"}, - {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7"}, - {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb"}, - {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f"}, - {file = "multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f"}, - {file = "multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885"}, - {file = "multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c"}, - {file = "multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000"}, - {file = "multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63"}, - {file = "multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718"}, - {file = "multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2"}, - {file = "multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e"}, - {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064"}, - {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e"}, - {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd"}, - {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a"}, - {file = "multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96"}, - {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e"}, - {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599"}, - {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394"}, - {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38"}, - {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9"}, - {file = "multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0"}, - {file = "multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13"}, - {file = "multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd"}, - {file = "multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827"}, - {file = "multidict-6.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c"}, - {file = "multidict-6.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40"}, - {file = "multidict-6.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851"}, - {file = "multidict-6.7.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687"}, - {file = "multidict-6.7.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5"}, - {file = "multidict-6.7.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb"}, - {file = "multidict-6.7.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6"}, - {file = "multidict-6.7.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e"}, - {file = "multidict-6.7.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e"}, - {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32"}, - {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c"}, - {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84"}, - {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329"}, - {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e"}, - {file = "multidict-6.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4"}, - {file = "multidict-6.7.0-cp39-cp39-win32.whl", hash = "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91"}, - {file = "multidict-6.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f"}, - {file = "multidict-6.7.0-cp39-cp39-win_arm64.whl", hash = "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546"}, - {file = "multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3"}, - {file = "multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, ] [[package]] @@ -1659,195 +1755,177 @@ files = [ [[package]] name = "numpy" -version = "2.3.4" +version = "2.5.2" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.11" +python-versions = ">=3.12" groups = ["main"] files = [ - {file = "numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36"}, - {file = "numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032"}, - {file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7"}, - {file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda"}, - {file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0"}, - {file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a"}, - {file = "numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1"}, - {file = "numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996"}, - {file = "numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667"}, - {file = "numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef"}, - {file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e"}, - {file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a"}, - {file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16"}, - {file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786"}, - {file = "numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc"}, - {file = "numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32"}, - {file = "numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197"}, - {file = "numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e"}, - {file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7"}, - {file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953"}, - {file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37"}, - {file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd"}, - {file = "numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646"}, - {file = "numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d"}, - {file = "numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a"}, - {file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6"}, - {file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7"}, - {file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0"}, - {file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f"}, - {file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64"}, - {file = "numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb"}, - {file = "numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c"}, - {file = "numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f"}, - {file = "numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b"}, - {file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7"}, - {file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2"}, - {file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52"}, - {file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26"}, - {file = "numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc"}, - {file = "numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9"}, - {file = "numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365"}, - {file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252"}, - {file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e"}, - {file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0"}, - {file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0"}, - {file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f"}, - {file = "numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d"}, - {file = "numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6"}, - {file = "numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d"}, - {file = "numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f"}, - {file = "numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a"}, + {file = "numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c"}, + {file = "numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03"}, + {file = "numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb"}, + {file = "numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414"}, + {file = "numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9"}, + {file = "numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8"}, + {file = "numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab"}, + {file = "numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7"}, + {file = "numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a"}, + {file = "numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4"}, + {file = "numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1"}, + {file = "numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15"}, + {file = "numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080"}, + {file = "numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740"}, + {file = "numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56"}, + {file = "numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3"}, + {file = "numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee"}, + {file = "numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59"}, + {file = "numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d"}, + {file = "numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4"}, + {file = "numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657"}, + {file = "numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2"}, + {file = "numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1"}, + {file = "numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8"}, + {file = "numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323"}, + {file = "numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e"}, + {file = "numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65"}, + {file = "numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee"}, + {file = "numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68"}, + {file = "numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb"}, + {file = "numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98"}, + {file = "numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d"}, + {file = "numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf"}, + {file = "numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1"}, + {file = "numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f"}, + {file = "numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf"}, + {file = "numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4"}, + {file = "numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce"}, + {file = "numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26"}, + {file = "numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac"}, + {file = "numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba"}, + {file = "numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884"}, + {file = "numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e"}, + {file = "numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f"}, + {file = "numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842"}, + {file = "numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6"}, + {file = "numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8"}, + {file = "numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69"}, + {file = "numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab"}, + {file = "numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514"}, + {file = "numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710"}, + {file = "numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617"}, + {file = "numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7"}, + {file = "numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788"}, + {file = "numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b"}, + {file = "numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e"}, + {file = "numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a"}, + {file = "numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8"}, + {file = "numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc"}, + {file = "numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec"}, + {file = "numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0"}, + {file = "numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2"}, + {file = "numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251"}, + {file = "numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12"}, + {file = "numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e"}, + {file = "numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860"}, ] [[package]] name = "packaging" -version = "25.0" +version = "26.3" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] name = "pandas" -version = "2.3.3" +version = "3.0.5" description = "Powerful data structures for data analysis, time series, and statistics" optional = false -python-versions = ">=3.9" +python-versions = ">=3.11" groups = ["main"] files = [ - {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, - {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, - {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, - {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, - {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, - {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, - {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, - {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, - {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, - {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, - {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, - {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, - {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, - {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, - {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, - {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, - {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, - {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, - {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, - {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, - {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, - {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, - {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, - {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, - {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, - {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, - {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, - {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, - {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, - {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, - {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, - {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, - {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, - {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, - {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, - {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, - {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, - {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, - {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, - {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, - {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, - {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, - {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, - {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, - {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, - {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, - {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, - {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, - {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, - {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, - {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, - {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, - {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, - {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, - {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, + {file = "pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282"}, + {file = "pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298"}, + {file = "pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899"}, + {file = "pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a"}, + {file = "pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928"}, + {file = "pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d"}, + {file = "pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a"}, + {file = "pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a"}, + {file = "pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d"}, + {file = "pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc"}, + {file = "pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc"}, + {file = "pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34"}, + {file = "pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6"}, + {file = "pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7"}, + {file = "pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce"}, + {file = "pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41"}, + {file = "pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49"}, + {file = "pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b"}, + {file = "pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3"}, + {file = "pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b"}, + {file = "pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be"}, + {file = "pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58"}, + {file = "pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee"}, + {file = "pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6"}, + {file = "pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e"}, + {file = "pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36"}, + {file = "pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c"}, + {file = "pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0"}, + {file = "pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b"}, + {file = "pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94"}, + {file = "pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da"}, + {file = "pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92"}, + {file = "pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85"}, + {file = "pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca"}, + {file = "pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da"}, + {file = "pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a"}, + {file = "pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040"}, + {file = "pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd"}, + {file = "pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c"}, + {file = "pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea"}, + {file = "pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c"}, + {file = "pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712"}, ] [package.dependencies] -numpy = {version = ">=1.26.0", markers = "python_version >= \"3.12\""} +numpy = [ + {version = ">=1.26.0", markers = "python_version < \"3.14\""}, + {version = ">=2.3.3", markers = "python_version >= \"3.14\""}, +] python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.7" +tzdata = {version = "*", markers = "sys_platform == \"win32\" or sys_platform == \"emscripten\""} [package.extras] -all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] -aws = ["s3fs (>=2022.11.0)"] -clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] -compression = ["zstandard (>=0.19.0)"] -computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] -feather = ["pyarrow (>=10.0.1)"] -fss = ["fsspec (>=2022.11.0)"] -gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] -hdf5 = ["tables (>=3.8.0)"] -html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] -mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] -parquet = ["pyarrow (>=10.0.1)"] -performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] -plot = ["matplotlib (>=3.6.3)"] -postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] -pyarrow = ["pyarrow (>=10.0.1)"] -spss = ["pyreadstat (>=1.2.0)"] -sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.9.2)"] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "adbc-driver-sqlite (>=1.2.0)", "beautifulsoup4 (>=4.12.3)", "bottleneck (>=1.4.2)", "fastparquet (>=2024.11.0)", "fsspec (>=2024.10.0)", "gcsfs (>=2024.10.0)", "html5lib (>=1.1)", "hypothesis (>=6.116.0)", "jinja2 (>=3.1.5)", "lxml (>=5.3.0)", "matplotlib (>=3.9.3)", "numba (>=0.60.0)", "numexpr (>=2.10.2)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.5)", "psycopg2 (>=2.9.10)", "pyarrow (>=13.0.0)", "pyiceberg (>=0.8.1)", "pymysql (>=1.1.1)", "pyreadstat (>=1.2.8)", "pytest (>=8.3.4)", "pytest-xdist (>=3.6.1)", "python-calamine (>=0.3.0)", "pytz (>=2020.1)", "pyxlsb (>=1.0.10)", "qtpy (>=2.4.2)", "s3fs (>=2024.10.0)", "scipy (>=1.14.1)", "tables (>=3.10.1)", "tabulate (>=0.9.0)", "xarray (>=2024.10.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.2.0)", "zstandard (>=0.23.0)"] +aws = ["s3fs (>=2024.10.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.4.2)"] +compression = ["zstandard (>=0.23.0)"] +computation = ["scipy (>=1.14.1)", "xarray (>=2024.10.0)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.5)", "python-calamine (>=0.3.0)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.2.0)"] +feather = ["pyarrow (>=13.0.0)"] +fss = ["fsspec (>=2024.10.0)"] +gcp = ["gcsfs (>=2024.10.0)"] +hdf5 = ["tables (>=3.10.1)"] +html = ["beautifulsoup4 (>=4.12.3)", "html5lib (>=1.1)", "lxml (>=5.3.0)"] +iceberg = ["pyiceberg (>=0.8.1)"] +mysql = ["SQLAlchemy (>=2.0.36)", "pymysql (>=1.1.1)"] +output-formatting = ["jinja2 (>=3.1.5)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=13.0.0)"] +performance = ["bottleneck (>=1.4.2)", "numba (>=0.60.0)", "numexpr (>=2.10.2)"] +plot = ["matplotlib (>=3.9.3)"] +postgresql = ["SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "psycopg2 (>=2.9.10)"] +pyarrow = ["pyarrow (>=13.0.0)"] +spss = ["pyreadstat (>=1.2.8)"] +sql-other = ["SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "adbc-driver-sqlite (>=1.2.0)"] +test = ["hypothesis (>=6.116.0)", "pytest (>=8.3.4,<9.1)", "pytest-xdist (>=3.6.1)"] +timezone = ["pytz (>=2020.1)"] +xml = ["lxml (>=5.3.0)"] [[package]] name = "pluggy" @@ -1867,161 +1945,163 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "propcache" -version = "0.4.1" +version = "0.5.2" description = "Accelerated property cache" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, - {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, - {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, - {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, - {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, - {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, - {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, - {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, - {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, - {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, - {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, - {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, - {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, - {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, - {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, - {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, - {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, - {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, - {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, - {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, - {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, - {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, - {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, - {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, - {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, - {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, - {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, - {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, - {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, -] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["dev"] -files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, + {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, + {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, + {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, + {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, + {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, + {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, + {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, + {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, + {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, + {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, + {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, + {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, + {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, + {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, + {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, + {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, + {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, + {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, + {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, + {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, + {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, + {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, + {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, ] [[package]] name = "pycparser" -version = "2.23" +version = "3.0" description = "C parser in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" files = [ - {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, - {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] +[[package]] +name = "pygments" +version = "2.21.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pyjwt" version = "2.13.0" @@ -2064,15 +2144,15 @@ PyYAML = ">=6.0.1" [[package]] name = "pyreadline3" -version = "3.5.4" +version = "3.5.6" description = "A python implementation of GNU readline." optional = false python-versions = ">=3.8" groups = ["main"] markers = "sys_platform == \"win32\"" files = [ - {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, - {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, + {file = "pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d"}, + {file = "pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf"}, ] [package.extras] @@ -2122,48 +2202,66 @@ files = [ [[package]] name = "pytest" -version = "6.2.5" +version = "9.1.1" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, ] [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -toml = "*" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" -version = "6.3.0" +version = "7.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749"}, - {file = "pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2"}, + {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, + {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, ] [package.dependencies] -coverage = {version = ">=7.5", extras = ["toml"]} +coverage = {version = ">=7.10.6", extras = ["toml"]} pluggy = ">=1.2" -pytest = ">=6.2.5" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, + {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, +] + +[package.dependencies] +execnet = ">=2.1" +pytest = ">=7.0.0" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] [[package]] name = "python-dateutil" @@ -2182,29 +2280,14 @@ six = ">=1.5" [[package]] name = "python-json-logger" -version = "4.0.0" +version = "4.2.0" description = "JSON Log Formatter for the Python Logging Package" optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2"}, - {file = "python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f"}, -] - -[package.extras] -dev = ["backports.zoneinfo ; python_version < \"3.9\"", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec ; implementation_name != \"pypy\"", "mypy", "orjson ; implementation_name != \"pypy\"", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] - -[[package]] -name = "pytz" -version = "2025.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, + {file = "python_json_logger-4.2.0-py3-none-any.whl", hash = "sha256:158a52126fcd6869e09574d2b66272666f3dc8f468c62637ef9a1fa883719cb9"}, + {file = "python_json_logger-4.2.0.tar.gz", hash = "sha256:e371ebe22ec01e289850102091a2b1f6fc9e655c7f1f5f29073936756c290afa"}, ] [[package]] @@ -2292,25 +2375,25 @@ files = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.34.2" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] -certifi = ">=2017.4.17" +certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "requests-mock" @@ -2332,14 +2415,14 @@ fixture = ["fixtures"] [[package]] name = "requirements-parser" -version = "0.13.0" +version = "0.13.1" description = "This is a small Python module for parsing Pip requirement files." optional = false python-versions = "<4.0,>=3.8" groups = ["dev"] files = [ - {file = "requirements_parser-0.13.0-py3-none-any.whl", hash = "sha256:2b3173faecf19ec5501971b7222d38f04cb45bb9d87d0ad629ca71e2e62ded14"}, - {file = "requirements_parser-0.13.0.tar.gz", hash = "sha256:0843119ca2cb2331de4eb31b10d70462e39ace698fd660a915c247d2301a4418"}, + {file = "requirements_parser-0.13.1-py3-none-any.whl", hash = "sha256:6e385663eb32589d16e5b22bb6e5251a57908e73803ffff438b53cd6ea2056e0"}, + {file = "requirements_parser-0.13.1.tar.gz", hash = "sha256:78811383b2089b6c5197a1431bc2c12ff950245edca39a23eea3460782038dd3"}, ] [package.dependencies] @@ -2347,14 +2430,14 @@ packaging = ">=23.2" [[package]] name = "setuptools" -version = "80.9.0" +version = "80.10.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, + {file = "setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173"}, + {file = "setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70"}, ] [package.extras] @@ -2378,28 +2461,16 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - [[package]] name = "starlette" -version = "1.3.1" +version = "1.6.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6"}, - {file = "starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0"}, + {file = "starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c"}, + {file = "starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b"}, ] [package.dependencies] @@ -2409,38 +2480,83 @@ anyio = ">=3.6.2,<5" full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] [[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" +name = "tomli" +version = "2.4.1" +description = "A lil' TOML parser" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.8" groups = ["dev"] -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +markers = "python_version < \"3.15\"" +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, ] [[package]] name = "tqdm" -version = "4.67.1" +version = "4.70.0" description = "Fast, Extensible Progress Meter" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, + {file = "tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953"}, + {file = "tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] +discord = ["envwrap", "requests"] notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] +slack = ["envwrap", "slack-sdk"] +telegram = ["envwrap", "requests"] [[package]] name = "truststore" @@ -2449,6 +2565,7 @@ description = "Verify certificates using native system trust stores" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "sys_platform != \"emscripten\"" files = [ {file = "truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981"}, {file = "truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301"}, @@ -2456,14 +2573,14 @@ files = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] [[package]] @@ -2484,45 +2601,46 @@ typing-extensions = ">=3.7.4" [[package]] name = "tzdata" -version = "2025.2" +version = "2026.3" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] +markers = "sys_platform == \"win32\" or sys_platform == \"emscripten\"" files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, + {file = "tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931"}, + {file = "tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415"}, ] [[package]] name = "urllib3" -version = "2.5.0" +version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "uvicorn" -version = "0.49.0" +version = "0.52.3" description = "The lightning-fast ASGI server." optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"dpop\"" files = [ - {file = "uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f"}, - {file = "uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3"}, + {file = "uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c"}, + {file = "uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58"}, ] [package.dependencies] @@ -2530,7 +2648,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=10.4)"] +standard = ["httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=13.0)"] [[package]] name = "werkzeug" @@ -2552,154 +2670,128 @@ watchdog = ["watchdog (>=2.3)"] [[package]] name = "xmltodict" -version = "0.13.0" +version = "0.15.1" description = "Makes working with XML feel like you are working with JSON" optional = false -python-versions = ">=3.4" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, - {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, + {file = "xmltodict-0.15.1-py2.py3-none-any.whl", hash = "sha256:dcd84b52f30a15be5ac4c9099a0cb234df8758624b035411e329c5c1e7a49089"}, + {file = "xmltodict-0.15.1.tar.gz", hash = "sha256:3d8d49127f3ce6979d40a36dbcad96f8bab106d232d24b49efdd4bd21716983c"}, ] [[package]] name = "yarl" -version = "1.22.0" +version = "1.24.5" description = "Yet another URL library" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"}, - {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"}, - {file = "yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467"}, - {file = "yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea"}, - {file = "yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"}, - {file = "yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e"}, - {file = "yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca"}, - {file = "yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b"}, - {file = "yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520"}, - {file = "yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8"}, - {file = "yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c"}, - {file = "yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67"}, - {file = "yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95"}, - {file = "yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d"}, - {file = "yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62"}, - {file = "yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03"}, - {file = "yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249"}, - {file = "yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da"}, - {file = "yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2"}, - {file = "yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79"}, - {file = "yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c"}, - {file = "yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e"}, - {file = "yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27"}, - {file = "yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8"}, - {file = "yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b"}, - {file = "yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed"}, - {file = "yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2"}, - {file = "yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff"}, - {file = "yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, + {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, + {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, + {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, + {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, + {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, + {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, + {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, + {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, + {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, + {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, + {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, + {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, + {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, + {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, ] [package.dependencies] @@ -2709,23 +2801,23 @@ propcache = ">=0.2.1" [[package]] name = "zipp" -version = "3.23.0" +version = "4.1.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, + {file = "zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f"}, + {file = "zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] +enabler = ["pytest-enabler (>=3.4)"] test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [extras] dpop = ["uvicorn"] @@ -2734,4 +2826,4 @@ fhir = ["fhirclient"] [metadata] lock-version = "2.1" python-versions = ">=3.13, <4" -content-hash = "247a56da10f5b59f9ded0caab898d67b4a0bf04f16218de44b4cd864fc2b64b6" +content-hash = "4021e5294293b73b7e341d0f029afe3c2db21cbf106cb95fed42ac22c5512493" diff --git a/pyproject.toml b/pyproject.toml index f47477216..0e41bc3c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,13 +58,8 @@ dpop = ["uvicorn"] [tool.poetry.group.dev.dependencies] pytest = ">=6.0.0" pytest-cov = "*" +pytest-xdist = "*" requests-mock = "*" -cdisutilstest = { git = "https://github.com/uc-cdis/cdisutils-test.git", tag = "2.0.0" } -# Can't be locked while authutils is pinned to the feat/dpop branch (8.1.0): -# indexd 5.0.4 requires authutils <7.0.0. tests/conftest.py still imports it, so -# install it by hand until authutils 8 is released and indexd picks it up. -# TODO FIXME -# indexd = { git = "https://github.com/uc-cdis/indexd.git", tag = "5.0.4" } deptry = ">=0.23.1" [tool.poetry.scripts] @@ -78,7 +73,27 @@ build-backend = "poetry.masonry.api" [tool.pytest.ini_options] minversion = "6.0" -addopts = "-vv" +# Run tests in parallel (as much as possible) by default. +# +# `load`, xdist's default, hands each test to whichever worker is free, so two tests +# from the same module can run at once in different processes. `loadscope` instead +# groups tests by class, or by module for tests outside one, and gives each whole +# group to a single worker, so a module's tests stay in order in one process. +# +# `--dist loadscope` MUST accompany `-n`. Why? Some modules are not independent +# test-by-test: +# - the fixed TMP_FOLDER in gen3/tools/indexing/download_manifest.py, which the +# library wipes on entry and then collects with a glob +# - the ~/.cache/gen3 token cache, keyed by API key rather than by test +# - download_tests/test_resolvers.py, where one test reads a DRS cache file that an +# earlier test in the module wrote +# - others? there might be... +# +# Under `load` these corrupt each other or go missing, and which tests fail moves +# around from run to run. Thus, `loadscope` keeps things safe. +# +# Pass `-n0` to run serially if needed for debugging. +addopts = "-vv --dist loadscope -n auto" testpaths = [ "tests", ] diff --git a/tests/conftest.py b/tests/conftest.py index f118fe9f5..609011cac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,22 +1,15 @@ """ Conf Test for Gen3 test suite """ -from multiprocessing import Process -import multiprocessing + +from collections.abc import Generator from unittest.mock import patch import os import pytest -import requests from drsclient.client import DrsClient -from cdisutilstest.code.indexd_fixture import ( - setup_database, - clear_database, - create_user, -) + from gen3.cli.auth import endpoint -from indexd import get_app -from indexd.default_settings import settings from gen3.file import Gen3File from gen3.index import Gen3Index @@ -25,6 +18,7 @@ from gen3.auth import Gen3Auth from gen3.object import Gen3Object +from tests.fake_indexd import FakeIndexd os.makedirs("tests/outputs", exist_ok=True) @@ -117,83 +111,37 @@ def supported_protocol(request): return request.param -@pytest.fixture(scope="session") -def indexd_server(): - """ - Fixture copied from cdisutils-test and updated to mock Arborist +@pytest.fixture +def indexd_server() -> Generator[FakeIndexd, None, None]: """ + An empty in-memory indexd, with every HTTP client routed to it. - class MockServer(object): - def __init__(self, port): - self.port = port - self.baseurl = "http://localhost:{}".format(port) - - def run_indexd(port): - app = get_app() - app.run(host="localhost", port=port, debug=False) - - def wait_for_indexd_alive(port): - url = "http://localhost:{}".format(port) - try: - requests.get(url) - except requests.ConnectionError: - return wait_for_indexd_alive(port) - else: - return - - def wait_for_indexd_not_alive(port): - url = "http://localhost:{}".format(port) - try: - requests.get(url) - except requests.ConnectionError: - return - else: - return wait_for_indexd_not_alive(port) - - class MockArboristClient(object): - def auth_request(*args, **kwargs): - return True - - port = 8001 - settings["auth"].arborist = MockArboristClient() - indexd = Process(target=run_indexd, args=[port]) - # Add this line because OS X multiprocessing default is spawn which will cause pickling errors - # NOTE: fork is unstable and not technically supported on OS X, forking is only supported on Unix - # However explicitly setting default behavior to fork to pass unit test, only used for tests - # https://docs.python.org/3/library/multiprocessing.html - # https://github.com/pytest-dev/pytest-flask/issues/104 - multiprocessing.set_start_method("fork") - indexd.start() - wait_for_indexd_alive(port) - - yield MockServer(port=port) - - indexd.terminate() + Each test gets its own store, so there is no database to clear between them. + """ + fake_indexd = FakeIndexd() + with fake_indexd.serving_in_process(): + yield fake_indexd @pytest.fixture -def index_client(indexd_server): +def index_client(indexd_server: FakeIndexd) -> Gen3Index: """ - Handles getting all the docs from an - indexing endpoint. Currently this is changing from - signpost to indexd, so we'll use just indexd_client now. - I.E. test to a common interface this could be multiply our - tests: - https://docs.pytest.org/en/latest/fixture.html#parametrizing-fixtures + Gen3Index pointed at the fake indexd. """ - setup_database() + return Gen3Index(indexd_server.baseurl, ("admin", "admin"), service_location="") - try: - user = create_user("admin", "admin") - except Exception: - # assume user already exists, try using username and password for admin - user = ("admin", "admin") - client = Gen3Index(indexd_server.baseurl, user, service_location="") - - yield client +@pytest.fixture +def gen3_index_over_http() -> Generator[Gen3Index, None, None]: + """ + Gen3Index pointed at a fake indexd listening on a real socket. - clear_database() + For tools that shell out: a subprocess cannot see the in-process patching that + `indexd_server` relies on, so it needs a server it can actually connect to. + """ + fake = FakeIndexd() + with fake.serving_over_http(): + yield Gen3Index(fake.baseurl, ("admin", "admin"), service_location="") @pytest.fixture @@ -213,21 +161,11 @@ def gen3_query(gen3_auth): @pytest.fixture(scope="function") -def drs_client(indexd_server): - """ - Returns a DrsClient. This will delete any documents, - aliases, or users made by this - client after the test has completed. - Currently the default user is the admin user - Runs once per test. - """ - try: - user = create_user("user", "user") - except Exception: - user = ("user", "user") - client = DrsClient(baseurl=indexd_server.baseurl, auth=user) - yield client - clear_database() +def drs_client(indexd_server: FakeIndexd) -> DrsClient: + """ + Returns a DrsClient pointed at the fake indexd. + """ + return DrsClient(baseurl=indexd_server.baseurl, auth=("user", "user")) @pytest.fixture(scope="function") diff --git a/tests/dpop/__init__.py b/tests/dpop/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/dpop/conftest.py b/tests/dpop/conftest.py new file mode 100644 index 000000000..af8622815 --- /dev/null +++ b/tests/dpop/conftest.py @@ -0,0 +1,9 @@ +""" +Values shared by the DPoP tests: the fake commons and the token it hands out. + +Both the proxy tests and the Nextflow tests assert on these, so they live here to +keep the two files describing the same commons. +""" + +TASK_TOKEN = "test-task-token-abc123" +COMMONS = "https://gen3.example.com" diff --git a/tests/dpop/test_dpop.py b/tests/dpop/test_dpop.py new file mode 100644 index 000000000..d09628903 --- /dev/null +++ b/tests/dpop/test_dpop.py @@ -0,0 +1,1261 @@ +""" +Test the DPoP token exchange, local proxy, and `gen3 dpop` commands. + +The proxy is exercised over real HTTP: each test runs the ASGI app under uvicorn +in front of a local echo server, so assertions are made on what the upstream +actually received rather than on the proxy's internals. +""" + +import asyncio +import base64 +from contextlib import contextmanager +from dataclasses import dataclass +import gzip +import hashlib +import json +import logging as stdlib_logging +import socket +import time +from typing import Any, Generator, Iterator +from unittest.mock import MagicMock, patch + +import httpx2 +import pytest +import requests +from click.testing import CliRunner, Result +from joserfc import jwk + +from gen3.auth import Gen3AuthError +from gen3.cli.dpop import dpop +from gen3.dpop import ( + AsyncDPoPProxy, + MissingDPoPNonceError, + ProxyStartupError, + dpop_proxy_context, + exchange_api_key_for_task_token, + is_proxy_running, + logging as dpop_logging, + resolve_service_endpoints, + start_proxy_server, +) +from tests.dpop.conftest import COMMONS, TASK_TOKEN + +TOKEN_ENDPOINT = f"{COMMONS}/user/credentials/api/access_token" +TES_PATH = "/ga4gh/tes/v1/tasks" +S3_PATH = "/s3/bucket/key.txt" + + +class _StubAuth: + """Minimal stand-in for Gen3Auth covering what dpop_proxy_context reads.""" + + def __init__( + self, + api_key: str | None = "some-api-key", + endpoint: str = COMMONS, + ) -> None: + self.endpoint = endpoint + self._api_key = api_key + + def get_api_key(self) -> str | None: + """Return the configured API key.""" + return self._api_key + + def get_access_token(self) -> str: + """Return a placeholder user access token.""" + return "user-access-token" + + +def _api_key_expiring_in(seconds: int | None) -> str: + """ + Build an unsigned JWT shaped like a Gen3 API key with the given lifetime. + + Args: + seconds (int | None): Lifetime from now, or None to omit `exp` entirely. + + Returns: + str: The encoded, unsigned API key. + """ + + def encode(payload: dict) -> str: + raw = json.dumps(payload).encode() + return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") + + claims: dict[str, Any] = {"scope": ["fence"]} + if seconds is not None: + claims["exp"] = int(time.time()) + seconds + + header = encode({"alg": "RS256", "typ": "JWT"}) + return f"{header}.{encode(claims)}.signature-not-checked-by-decode_token" + + +@pytest.fixture +def ec_key() -> jwk.Key: + """Generate an ephemeral EC P-256 key like the proxy does at startup.""" + return jwk.generate_key("EC", "P-256", private=True) + + +@pytest.fixture +def dpop_logs() -> Iterator[list[stdlib_logging.LogRecord]]: + """Collect every record gen3.dpop logs at DEBUG during the test.""" + records: list[stdlib_logging.LogRecord] = [] + handler = stdlib_logging.Handler() + handler.emit = records.append + previous_level = dpop_logging.level + + dpop_logging.addHandler(handler) + dpop_logging.setLevel(stdlib_logging.DEBUG) + yield records + dpop_logging.removeHandler(handler) + dpop_logging.setLevel(previous_level) + + +@pytest.fixture +def proxy(ec_key) -> Iterator["_Proxy"]: + """Run AsyncDPoPProxy in front of the echo upstream, both on localhost.""" + upstream = _Upstream() + with _running(upstream) as upstream_port: + config = { + "KEY": ec_key, + "TASK_TOKEN": TASK_TOKEN, + "TES_ENDPOINT": f"http://127.0.0.1:{upstream_port}/ga4gh/tes", + "S3_ENDPOINT": f"http://127.0.0.1:{upstream_port}/s3", + "CACHED_NONCE": None, + } + app = AsyncDPoPProxy(config) + with _running(app) as port: + yield _Proxy( + upstream=upstream, + upstream_port=upstream_port, + config=config, + app=app, + url=f"http://127.0.0.1:{port}", + ) + + +class TestTaskTokenExchange: + """exchange_api_key_for_task_token against a cooperating token endpoint.""" + + def test_returns_the_task_token(self, ec_key, requests_mock): + """A successful exchange returns the token and no nonce.""" + requests_mock.post(TOKEN_ENDPOINT, json={"access_token": TASK_TOKEN}) + + assert _exchange(ec_key) == (TASK_TOKEN, None) + + def test_sends_a_dpop_proof_for_the_token_endpoint(self, ec_key, requests_mock): + """The request carries a proof bound to POST and the token endpoint.""" + requests_mock.post(TOKEN_ENDPOINT, json={"access_token": TASK_TOKEN}) + + _exchange(ec_key) + + payload = _jwt_segment(requests_mock.last_request.headers["DPoP"], 1) + assert payload["htm"] == "POST" + assert payload["htu"] == TOKEN_ENDPOINT + # No access token exists yet, so there is nothing to hash into ath. + assert "ath" not in payload + + def test_task_token_type_and_expiration_are_requested(self, ec_key, requests_mock): + """ + Token type and requested lifetime are sent as query parameters. + + Asserted against the raw URL because requests_mock's parsed `qs` + lowercases values, which would hide a change in the token type's case. + """ + requests_mock.post(TOKEN_ENDPOINT, json={"access_token": TASK_TOKEN}) + + _exchange(ec_key, task_token_expiration=900) + + url = requests_mock.last_request.url + assert "task_token=WORKFLOW" in url + assert "expires_in=900" in url + + def test_expiration_is_omitted_when_not_requested(self, ec_key, requests_mock): + """Without an explicit expiration the server default is left alone.""" + requests_mock.post(TOKEN_ENDPOINT, json={"access_token": TASK_TOKEN}) + + _exchange(ec_key) + + assert "expires_in" not in requests_mock.last_request.url + + +class TestTaskTokenExchangeNonceRetry: + """A token endpoint that demands a nonce before it will issue a token.""" + + def test_nonce_challenge_is_retried(self, ec_key, requests_mock): + """A use_dpop_nonce response is retried with the server-provided nonce.""" + requests_mock.post( + TOKEN_ENDPOINT, + [ + { + "status_code": 400, + "json": {"error": "use_dpop_nonce"}, + "headers": {"DPoP-Nonce": "as-nonce-1"}, + }, + {"status_code": 200, "json": {"access_token": TASK_TOKEN}}, + ], + ) + + assert _exchange(ec_key) == (TASK_TOKEN, "as-nonce-1") + payload = _jwt_segment(requests_mock.last_request.headers["DPoP"], 1) + assert payload["nonce"] == "as-nonce-1" + + def test_challenge_carrying_only_a_nonce_header_is_retried( + self, ec_key, requests_mock + ): + """A refusal with a nonce but no explanation is still read as a nonce demand.""" + requests_mock.post( + TOKEN_ENDPOINT, + [ + { + "status_code": 401, + "text": "", + "headers": {"DPoP-Nonce": "as-nonce-2"}, + }, + {"status_code": 200, "json": {"access_token": TASK_TOKEN}}, + ], + ) + + assert _exchange(ec_key) == (TASK_TOKEN, "as-nonce-2") + + def test_endless_nonce_challenge_gives_up(self, ec_key, requests_mock): + """An endpoint that never accepts a nonce ends in an error, not a loop.""" + requests_mock.post( + TOKEN_ENDPOINT, + status_code=400, + json={"error": "use_dpop_nonce"}, + headers={"DPoP-Nonce": "as-nonce-1"}, + ) + + _refusal(ec_key) + + assert requests_mock.call_count == 3 + + def test_challenge_without_a_nonce_header_raises(self, ec_key, requests_mock): + """A nonce demand with no DPoP-Nonce header is a server protocol error.""" + requests_mock.post( + TOKEN_ENDPOINT, status_code=400, json={"error": "use_dpop_nonce"} + ) + + with pytest.raises(MissingDPoPNonceError): + _exchange(ec_key) + + +class TestTaskTokenExchangeFailures: + """ + What the caller is told when the exchange fails. + + These assert only on data the fake server put in the response, never on this + SDK's own wording, so rephrasing an error message cannot break them. + """ + + def test_missing_access_token_raises(self, ec_key, requests_mock): + """A 200 without an access_token fails instead of returning None.""" + requests_mock.post(TOKEN_ENDPOINT, json={"unexpected": "shape"}) + + with pytest.raises(Gen3AuthError): + _exchange(ec_key) + + def test_unreachable_commons_raises_a_request_error(self, ec_key, requests_mock): + """A connection failure surfaces as a requests error, not a bare traceback.""" + requests_mock.post(TOKEN_ENDPOINT, exc=requests.ConnectionError("no route")) + + with pytest.raises(requests.RequestException): + _exchange(ec_key) + + def test_status_code_and_url_are_reported(self, ec_key, requests_mock): + """The error names the status and the endpoint that rejected the request.""" + requests_mock.post(TOKEN_ENDPOINT, status_code=403, json={"error": "denied"}) + + error = str(_refusal(ec_key)) + + assert "403" in error + assert TOKEN_ENDPOINT in error + + # `body` is passed straight to requests_mock as kwargs (json= or text=). + + @pytest.mark.parametrize( + "body,expected", + [ + # Whichever field fence puts its explanation in, it is quoted back. + pytest.param({"json": {"message": "no can do"}}, "no can do", id="message"), + pytest.param( + {"json": {"error_description": "no can do"}}, + "no can do", + id="error_description", + ), + pytest.param({"json": {"detail": "no can do"}}, "no can do", id="detail"), + pytest.param({"json": {"error": "no can do"}}, "no can do", id="error"), + # An HTML or plain-text body is included rather than dropped. + pytest.param( + {"text": "bad gateway"}, "bad gateway", id="html" + ), + # A JSON error with no field this SDK knows about is reported as-is. + pytest.param({"json": {"weird": ["shape"]}}, '"weird"', id="unknown_shape"), + # JSON that is not an object at all still has to survive the trip. + pytest.param({"json": ["no can do"]}, "no can do", id="json_array"), + ], + ) + def test_server_explanation_is_surfaced( + self, ec_key, requests_mock, body, expected + ): + """Whatever the server said reaches the caller.""" + requests_mock.post(TOKEN_ENDPOINT, status_code=400, **body) + + assert expected in str(_refusal(ec_key)) + + def test_empty_error_body_still_raises(self, ec_key, requests_mock): + """A refusal with no body at all is still an error, not a crash.""" + requests_mock.post(TOKEN_ENDPOINT, status_code=500, text="") + + _refusal(ec_key) + + def test_unrelated_error_is_not_retried_as_a_nonce_problem( + self, ec_key, requests_mock + ): + """A 400 carrying a nonce header but explaining something else is not retried.""" + requests_mock.post( + TOKEN_ENDPOINT, + status_code=400, + json={"message": "no can do"}, + headers={"DPoP-Nonce": "nonce-attached-to-everything"}, + ) + + error = str(_refusal(ec_key)) + + assert requests_mock.call_count == 1 + assert "no can do" in error + + +class TestApiKeyExpirationCheck: + """The lifetime a task token can be asked for is bounded by the API key.""" + + @pytest.fixture(autouse=True) + def token_endpoint(self, requests_mock) -> None: + """Answer any exchange that does reach the network with a task token.""" + requests_mock.post(TOKEN_ENDPOINT, json={"access_token": TASK_TOKEN}) + + def test_lifetime_within_the_api_key_is_allowed(self, ec_key, requests_mock): + """A lifetime the key can cover goes through untouched.""" + token, _ = _exchange( + ec_key, api_key=_api_key_expiring_in(7200), task_token_expiration=3600 + ) + + assert token == TASK_TOKEN + assert "expires_in=3600" in requests_mock.last_request.url + + @pytest.mark.parametrize( + "api_key_lifetime,requested_lifetime", + [ + pytest.param(3600, 345600, id="request_outlives_api_key"), + pytest.param(-60, 60, id="api_key_already_expired"), + ], + ) + def test_impossible_lifetime_is_refused_before_any_request( + self, ec_key, requests_mock, api_key_lifetime, requested_lifetime + ): + """A lifetime the API key cannot cover fails locally, with no round trip.""" + with pytest.raises(Gen3AuthError): + _exchange( + ec_key, + api_key=_api_key_expiring_in(api_key_lifetime), + task_token_expiration=requested_lifetime, + ) + + assert not requests_mock.called + + def test_no_requested_lifetime_skips_the_check(self, ec_key, requests_mock): + """Without an explicit lifetime the server picks one, so nothing is checked.""" + token, _ = _exchange(ec_key, api_key=_api_key_expiring_in(-60)) + + assert token == TASK_TOKEN + assert requests_mock.called + + @pytest.mark.parametrize( + "api_key", + [ + pytest.param("not-a-jwt", id="not_a_jwt"), + pytest.param("a.b.c", id="undecodable_segments"), + pytest.param("", id="empty"), + pytest.param(_api_key_expiring_in(None), id="no_exp_claim"), + ], + ) + def test_api_key_with_no_readable_expiration_defers_to_the_server( + self, ec_key, requests_mock, api_key + ): + """A key that cannot bound the lifetime is the token endpoint's problem.""" + token, _ = _exchange(ec_key, api_key=api_key, task_token_expiration=345600) + + assert token == TASK_TOKEN + assert requests_mock.called + + +class TestServiceEndpointResolution: + """Which TES and S3 endpoints the proxy is allowed to forward to.""" + + def test_endpoints_default_to_the_commons_that_issued_the_credentials(self): + """With no overrides, both endpoints sit on the commons named by the API key.""" + tes_endpoint, s3_endpoint = resolve_service_endpoints(_StubAuth()) + + assert tes_endpoint == f"{COMMONS}/ga4gh/tes" + assert s3_endpoint == f"{COMMONS}/workflows/s3" + + def test_overrides_on_the_same_commons_are_used_as_given(self): + """A commons that serves these APIs from elsewhere can say so.""" + endpoints = resolve_service_endpoints( + _StubAuth(), + tes_endpoint=f"{COMMONS}/ga4gh-tes/v2", + s3_endpoint=f"{COMMONS}/data/s3", + ) + + assert endpoints == (f"{COMMONS}/ga4gh-tes/v2", f"{COMMONS}/data/s3") + + @pytest.mark.parametrize( + "override", + [ + pytest.param( + {"tes_endpoint": "https://other.example.com/ga4gh/tes"}, id="tes" + ), + pytest.param({"s3_endpoint": "https://other.example.com/s3"}, id="s3"), + ], + ) + def test_an_endpoint_on_another_host_is_refused(self, override): + """ + The task token is only valid on the commons that issued it. + + Forwarding to any other host would hand that host a working credential, so + a mistyped endpoint has to fail rather than be honored. + """ + with pytest.raises(Gen3AuthError): + resolve_service_endpoints(_StubAuth(), **override) + + +class TestProxyLifecycle: + """Starting and stopping the background proxy server.""" + + @staticmethod + def _app(ec_key) -> AsyncDPoPProxy: + """A proxy app with no upstreams configured, for tests that only bind a port.""" + return AsyncDPoPProxy({"KEY": ec_key, "TASK_TOKEN": TASK_TOKEN}) + + def test_os_assigned_port_is_reported(self, ec_key): + """With no port requested, the bound port is returned to the caller.""" + with _running(self._app(ec_key)) as port: + assert port > 0 + assert is_proxy_running(port) + + def test_requested_port_is_honored(self, ec_key): + """An explicit port is used as given.""" + requested = _unused_port() + + with _running(self._app(ec_key), port=requested) as port: + assert port == requested + + def test_shutdown_releases_the_port(self, ec_key): + """Stopping the server closes its listening socket and ends the thread.""" + server, thread, port = start_proxy_server(self._app(ec_key), log_level="error") + server.should_exit = True + thread.join(timeout=10) + + assert not thread.is_alive() + assert not is_proxy_running(port) + + def test_context_manager_starts_and_stops_the_proxy(self, requests_mock): + """dpop_proxy_context yields a live proxy and tears it down on exit.""" + requests_mock.post(TOKEN_ENDPOINT, json={"access_token": TASK_TOKEN}) + + with dpop_proxy_context(auth=_StubAuth()) as (task_token, port): + assert task_token == TASK_TOKEN + assert is_proxy_running(port) + + assert not is_proxy_running(port) + + def test_taken_port_fails_fast(self, ec_key): + """ + A port that is in use is caught up front, not after the startup timeout. + + Without the pre-check this still raises ProxyStartupError, just ten seconds + later, so the elapsed time is the only thing that tells the two apart. + """ + with _running(self._app(ec_key)) as port: + started = time.monotonic() + + with pytest.raises(ProxyStartupError): + start_proxy_server(self._app(ec_key), port=port, log_level="error") + + assert time.monotonic() - started < 1 + + @pytest.mark.parametrize( + "auth", + [ + pytest.param(_StubAuth(api_key=None), id="no_api_key"), + pytest.param(_StubAuth(endpoint=""), id="no_endpoint"), + ], + ) + def test_context_manager_requires_credentials(self, auth, requests_mock): + """Incomplete credentials fail before anything is sent to the commons.""" + requests_mock.post(TOKEN_ENDPOINT, json={"access_token": TASK_TOKEN}) + + with pytest.raises(Gen3AuthError): + with dpop_proxy_context(auth=auth): + pass + + assert not requests_mock.called + + +class TestProxyRouting: + """Where the proxy forwards each incoming path.""" + + @pytest.mark.parametrize( + "incoming_path,upstream_path", + [ + # The endpoints already end in the prefix, so it must not be doubled. + (TES_PATH, TES_PATH), + (S3_PATH, S3_PATH), + ], + ) + def test_path_is_routed_without_doubling_the_prefix( + self, proxy, incoming_path, upstream_path + ): + """Each incoming path reaches the right upstream at the right path.""" + response = proxy.get(incoming_path) + + assert response.status_code == 200 + assert proxy.upstream.last_request["path"] == upstream_path + + def test_method_and_query_string_are_preserved(self, proxy): + """The upstream sees the client's method and query parameters unchanged.""" + proxy.request("DELETE", TES_PATH, params={"page": "2"}) + + assert proxy.upstream.last_request["method"] == "DELETE" + assert proxy.upstream.last_request["query"] == "page=2" + + @pytest.mark.parametrize( + "path", + [ + pytest.param("/bucket/key.txt", id="root_is_not_a_shortcut_for_s3"), + pytest.param("/user/credentials/api/access_token", id="other_gen3_service"), + pytest.param("/ga4gh/drs/v1/objects/abc", id="other_ga4gh_api"), + pytest.param("/", id="root"), + ], + ) + def test_paths_outside_tes_and_s3_are_refused(self, proxy, path, dpop_logs): + """ + Only TES and S3 traffic belongs on this proxy; the rest never leaves. + + The task token and a valid proof are attached to everything forwarded, so + an unrecognized path must not be guessed at. + """ + response = proxy.get(path) + + assert response.status_code == 404 + assert proxy.upstream.requests == [] + # A local 404 is indistinguishable from an upstream one without this. + assert len(_warnings(dpop_logs)) >= 1 + + @pytest.mark.parametrize( + "path", + [ + pytest.param("/s3/../user/credentials/api/access_token", id="literal"), + pytest.param("/s3/%2e%2e/user/x", id="percent_encoded"), + pytest.param("/s3/bucket/../../user/x", id="nested"), + pytest.param("/ga4gh/tes/../../user/x", id="from_tes"), + ], + ) + def test_paths_that_escape_their_upstream_base_are_refused( + self, proxy, path, dpop_logs + ): + """ + A path may not use `..` to reach a commons endpoint outside its base. + + uvicorn hands over the decoded path and httpx resolves dot segments when + it builds the request, so without this the proxy would sign a request to + any path on the commons with the task token. + """ + assert proxy.raw_get_status(path) == 404 + assert proxy.upstream.requests == [] + # This one is worth noticing in a log: it means something is sending paths + # that try to leave the two endpoints the task token is meant for. + assert len(_warnings(dpop_logs)) >= 1 + + +class TestProxyHeaders: + """What the proxy sends upstream on behalf of the client.""" + + def test_tes_credentials_are_replaced(self, proxy): + """On the TES route the proxy supplies both credentials itself.""" + proxy.get( + TES_PATH, + headers={ + "Authorization": "Bearer some-other-token", + "DPoP": "client-supplied-proof", + }, + ) + + headers = proxy.upstream.last_request["headers"] + assert headers["authorization"] == f"DPoP {TASK_TOKEN}" + assert headers["dpop"] != "client-supplied-proof" + + def test_s3_signature_survives_the_hop(self, proxy): + """On the S3 route the client's signed `Authorization` is forwarded as-is.""" + # Gen3's S3 endpoint reads the token out of the SigV4 `Credential` field, so + # replacing the header with the DPoP scheme leaves it no token to find. + signed = f"AWS4-HMAC-SHA256 Credential={TASK_TOKEN}/20240101/us-east-1/s3/aws4_request" + proxy.get( + S3_PATH, + headers={"Authorization": signed, "DPoP": "client-supplied-proof"}, + ) + + headers = proxy.upstream.last_request["headers"] + assert headers["authorization"] == signed + assert headers["dpop"] != "client-supplied-proof" + + def test_unrelated_headers_are_forwarded(self, proxy): + """Client headers the proxy has no opinion about are passed through.""" + proxy.put( + S3_PATH, + headers={ + "x-amz-content-sha256": "UNSIGNED-PAYLOAD", + "x-amz-date": "20240101T000000Z", + }, + data=b"payload", + ) + + headers = proxy.upstream.last_request["headers"] + assert headers["x-amz-content-sha256"] == "UNSIGNED-PAYLOAD" + assert headers["x-amz-date"] == "20240101T000000Z" + + def test_host_header_names_the_upstream(self, proxy): + """Host is rebuilt for the upstream rather than forwarded from the client.""" + proxy.get(TES_PATH) + + assert ( + proxy.upstream.last_request["headers"]["host"] + == f"127.0.0.1:{proxy.upstream_port}" + ) + + def test_connection_specific_headers_are_not_forwarded(self, proxy): + """ + Connection-specific fields describe the client's connection, not ours. + + An intermediary strips them rather than passing them on (RFC 9110 7.6.1). + """ + proxy.get(TES_PATH, headers={"te": "trailers", "upgrade": "h2c"}) + + headers = proxy.upstream.last_request["headers"] + assert "te" not in headers + assert "upgrade" not in headers + + def test_task_token_is_never_logged(self, proxy, dpop_logs): + """`-vv` output could get pasted into bug reports, so it must carry no credential.""" + proxy.get(TES_PATH) + + logged = "\n".join(r.getMessage() for r in dpop_logs) + assert "DPoP proof" in logged, "debug logging did not run" + assert TASK_TOKEN not in logged + + +class TestProxyDPoPProof: + """The proof the upstream receives with each proxied request.""" + + def test_proof_binds_the_request_and_the_task_token(self, proxy): + """htm, htu and ath describe the request the upstream actually got.""" + proxy.request("POST", TES_PATH, params={"page": "2"}, json={"a": 1}) + + payload = proxy.proof() + assert payload["htm"] == "POST" + assert payload["htu"] == f"http://127.0.0.1:{proxy.upstream_port}{TES_PATH}" + # htu carries scheme, host and path only, per RFC 9449 4.2. + assert "?" not in payload["htu"] + assert payload["ath"] == _expected_ath(TASK_TOKEN) + + def test_proof_header_advertises_the_public_key_only(self, proxy): + """The embedded JWK is an EC public key, never the private component.""" + proxy.get(TES_PATH) + + header = proxy.proof(index=0) + assert header["typ"] == "dpop+jwt" + assert header["alg"] == "ES256" + assert header["jwk"]["kty"] == "EC" + assert "d" not in header["jwk"] + + def test_each_request_gets_a_fresh_proof(self, proxy): + """jti is unique per proxied request so proofs cannot be replayed.""" + proxy.get(TES_PATH) + first_jti = proxy.proof()["jti"] + proxy.get(TES_PATH) + + assert proxy.proof()["jti"] != first_jti + + def test_cached_nonce_is_included_in_the_proof(self, proxy): + """A nonce already known at startup is carried in every proof.""" + proxy.config["CACHED_NONCE"] = "nonce-from-token-exchange" + proxy.get(TES_PATH) + + assert proxy.proof()["nonce"] == "nonce-from-token-exchange" + + +class TestProxyBodies: + """Request and response bodies through the proxy.""" + + def test_chunked_upload_is_reframed_with_a_content_length(self, proxy): + """ + A chunked upload from the client goes upstream with a content-length. + + S3-compatible backends reject chunked PUTs, so the client's framing cannot + be passed through. + """ + + def chunks(): + for _ in range(3): + yield b"y" * 100 + + response = proxy.put(S3_PATH, data=chunks()) + + assert response.status_code == 200 + request = proxy.upstream.last_request + assert request["body_length"] == 300 + assert request["headers"]["content-length"] == "300" + assert "transfer-encoding" not in request["headers"] + + def test_bodyless_request_carries_no_framing_headers(self, proxy): + """A GET goes upstream without content-length or transfer-encoding.""" + proxy.get(TES_PATH) + + request = proxy.upstream.last_request + assert request["body_length"] == 0 + assert "content-length" not in request["headers"] + assert "transfer-encoding" not in request["headers"] + + def test_large_upload_arrives_intact(self, proxy): + """A body larger than the in-memory spool threshold is forwarded whole.""" + size = 12 * 1024 * 1024 + + response = proxy.put("/s3/bucket/big", data=b"q" * size, timeout=120) + + assert response.status_code == 200 + assert proxy.upstream.last_request["body_length"] == size + + def test_large_download_arrives_intact(self, proxy): + """A multi-megabyte response is streamed back with its content-length.""" + proxy.upstream.response_megabytes = 8 + + with proxy.get("/s3/bucket/big", stream=True, timeout=120) as response: + content_length = response.headers["content-length"] + received = sum(len(chunk) for chunk in response.iter_content(65536)) + + assert received == 8 * 1024 * 1024 + assert content_length == str(received) + + +class TestProxyNonceRetry: + """How the proxy answers a use_dpop_nonce challenge.""" + + def test_challenge_is_retried_with_the_server_nonce(self, proxy): + """A nonce challenge is retried transparently and the client sees success.""" + proxy.upstream.nonce_challenges_to_send = 1 + + response = proxy.get(TES_PATH) + + assert response.status_code == 200 + assert proxy.proof()["nonce"] == "server-nonce-1" + + def test_body_is_replayed_on_retry(self, proxy): + """The retried request carries the same body as the original.""" + proxy.upstream.nonce_challenges_to_send = 1 + + response = proxy.put(S3_PATH, data=b"x" * 4096) + + assert response.status_code == 200 + assert proxy.upstream.last_request["body_length"] == 4096 + + def test_persistent_challenge_is_returned_to_the_client(self, proxy): + """After the retry budget is spent, the upstream's 401 reaches the client.""" + proxy.upstream.nonce_challenges_to_send = 99 + + response = proxy.get(TES_PATH) + + assert response.status_code == 401 + assert response.json() == {"error": "use_dpop_nonce"} + assert proxy.upstream.nonce_challenges_sent == 3 + + def test_challenge_without_a_usable_nonce_becomes_a_502(self, proxy): + """A server demanding a nonce but sending an empty one is a protocol error.""" + proxy.upstream.nonce_challenges_to_send = 1 + proxy.upstream.nonce_header = b"" + + response = proxy.get(TES_PATH) + + assert response.status_code == 502 + + +class TestProxyUpstreamFailures: + """How upstream problems are reported to the client.""" + + @pytest.mark.parametrize("status", [400, 401, 403, 500]) + def test_upstream_error_reaches_the_client_intact(self, proxy, status): + """ + An error the upstream raised is forwarded with its status and body. + + A 400 or 401 is read in full before it can be told apart from a nonce + challenge, so those two take the buffered path and the others are streamed; + either way the client has to end up with the same status and explanation. + """ + proxy.upstream.error_status = status + proxy.upstream.error_body = b'{"error": "invalid_token"}' + + response = proxy.get(TES_PATH) + + assert response.status_code == status + assert response.json() == {"error": "invalid_token"} + + def test_compressed_upstream_error_is_reframed_before_forwarding(self, proxy): + """ + A compressed error body is forwarded decoded, without its stale headers. + + Classifying a 400 decodes the body, so leaving `content-encoding` in place + would tell the client to gunzip plain text. + """ + body = b'{"error": "invalid_request"}' + proxy.upstream.error_status = 400 + proxy.upstream.error_body = gzip.compress(body) + proxy.upstream.error_content_encoding = b"gzip" + + response = proxy.get(TES_PATH) + + assert response.status_code == 400 + assert response.content == body + assert "content-encoding" not in response.headers + + def test_unreachable_upstream_becomes_a_502(self, proxy): + """A connection failure is reported as a bad gateway, not a crash.""" + proxy.config["S3_ENDPOINT"] = f"http://127.0.0.1:{_unused_port()}/s3" + + response = proxy.get(S3_PATH) + + assert response.status_code == 502 + assert "Upstream request failed" in response.text + + +class TestProxyUpstreamTimeouts: + """How long the proxy waits on an upstream before giving up.""" + + def test_upstream_that_never_answers_becomes_a_504(self, proxy): + """An upstream that stops responding times out instead of hanging forever.""" + proxy.upstream.slow_response_seconds = 0.4 + proxy.app._client.timeout = httpx2.Timeout(0.1) + + response = proxy.get(TES_PATH) + + assert response.status_code == 504 + + @pytest.mark.parametrize("action", ["read", "write"]) + def test_upstream_transfers_outlive_the_httpx_default_timeout(self, ec_key, action): + """ + The proxy waits longer than httpx2's default before giving up on a transfer. + + A workflow moves objects that take far longer than httpx2's 5s default, so + inheriting that default would truncate them. + """ + default = getattr(httpx2.AsyncClient().timeout, action) + configured = getattr(AsyncDPoPProxy({"KEY": ec_key})._client.timeout, action) + + assert configured is not None, f"no {action} timeout leaves a transfer hanging" + assert configured > default + + +class TestProxyAsgiScopes: + """Which ASGI protocols the proxy app serves.""" + + def test_non_http_scope_is_rejected(self, proxy): + """Only HTTP scopes are served; anything else gets a 404.""" + sent = _run_asgi(proxy.app, {"type": "websocket"}) + + assert sent[0]["status"] == 404 + + +class TestProxyStartCommand: + """`gen3 dpop proxy start` option wiring and error reporting.""" + + @staticmethod + def _invoke( + args: list[str], auth_factory: Any = None, wait: Any = None + ) -> tuple[Result, Any, MagicMock]: + """ + Run the command with the proxy stubbed out and the block short-circuited. + + Args: + args (list[str]): Arguments after `proxy start`. + auth_factory (Any): Factory to put in the context, or None for a mock. + wait (Any): Side effect for the wait the command blocks on, so a test + can interrupt it instead of returning from it. + + Returns: + tuple[Result, Any, MagicMock]: The CLI result, the auth factory, and + the patched dpop_proxy_context. + """ + auth_factory = auth_factory or MagicMock() + with patch("gen3.cli.dpop.dpop_proxy_context") as proxy_context, patch( + "threading.Event.wait", side_effect=wait, return_value=None + ): + proxy_context.return_value.__enter__.return_value = (TASK_TOKEN, 8000) + result = CliRunner().invoke( + dpop, + ["proxy", "start"] + args, + obj={"auth_factory": auth_factory}, + ) + return result, auth_factory, proxy_context + + @staticmethod + def _invoke_with_failure(error: BaseException) -> Result: + """Run the command with the proxy raising `error` on startup.""" + with patch("gen3.cli.dpop.dpop_proxy_context", side_effect=error): + return CliRunner().invoke( + dpop, ["proxy", "start"], obj={"auth_factory": MagicMock()} + ) + + @pytest.mark.parametrize( + "args,expected", + [ + # Defaults: no --port means "let the OS pick", hence None not 0. + ([], {"port": None, "task_token_type": "WORKFLOW"}), + (["--port", "8123"], {"port": 8123}), + (["--task-token-type", "TASK"], {"task_token_type": "TASK"}), + (["--task-token-expiration", "1800"], {"task_token_expiration": 1800}), + ( + ["--tes-endpoint", "https://gen3.example.com/ga4gh/tes"], + {"tes_endpoint": "https://gen3.example.com/ga4gh/tes"}, + ), + ( + ["--s3-endpoint", "https://gen3.example.com/s3"], + {"s3_endpoint": "https://gen3.example.com/s3"}, + ), + ], + ) + def test_options_reach_the_proxy_context(self, args, expected): + """Each option is forwarded to dpop_proxy_context unchanged.""" + result, _, proxy_context = self._invoke(args) + + assert result.exit_code == 0 + assert expected.items() <= proxy_context.call_args.kwargs.items() + + @pytest.mark.parametrize( + "env,args,expected_port", + [ + pytest.param({}, [], None, id="unset_lets_the_os_pick"), + pytest.param({"GEN3_DPOP_PROXY_PORT": "8000"}, [], 8000, id="env_pins_it"), + pytest.param( + {"GEN3_DPOP_PROXY_PORT": "8000"}, + ["--port", "9111"], + 9111, + id="flag_beats_env", + ), + # `export GEN3_DPOP_PROXY_PORT=` is how people unset it, so it must + # not become a usage error. + pytest.param({"GEN3_DPOP_PROXY_PORT": ""}, [], None, id="empty_is_unset"), + ], + ) + def test_port_can_be_pinned_with_an_environment_variable( + self, env, args, expected_port, monkeypatch + ): + """GEN3_DPOP_PROXY_PORT saves retyping --port on every run.""" + for name, value in env.items(): + monkeypatch.setenv(name, value) + + result, _, proxy_context = self._invoke(args) + + assert result.exit_code == 0 + assert proxy_context.call_args.kwargs["port"] == expected_port + + def test_the_bound_port_is_reported_to_the_user(self, caplog): + """ + The port the proxy came up on is announced once it is serving. + + With no port requested the OS picks one, so this line is the only place a + client can learn where to send its requests. + """ + with caplog.at_level(stdlib_logging.WARNING, logger="gen3.cli.dpop"): + result, _, _ = self._invoke([]) + + assert result.exit_code == 0 + assert "127.0.0.1:8000" in caplog.text + + def test_credentials_come_from_the_root_group(self): + """Auth is resolved through the lazy factory the root group installs.""" + result, auth_factory, proxy_context = self._invoke([]) + + assert result.exit_code == 0 + auth_factory.get.assert_called_once_with() + assert proxy_context.call_args.kwargs["auth"] is auth_factory.get.return_value + + def test_ctrl_c_stops_the_proxy_without_an_error(self): + """Ctrl+C is the documented way to stop the proxy, so it is not a failure.""" + result, _, _ = self._invoke([], wait=KeyboardInterrupt) + + assert result.exit_code == 0 + + @pytest.mark.parametrize( + "error", + [ + Gen3AuthError("Requested a task token lifetime of 345600 seconds"), + ProxyStartupError("Something is already listening on 127.0.0.1:8000"), + MissingDPoPNonceError("Server did not provide a nonce"), + requests.ConnectionError("Failed to establish a new connection"), + ImportError("The DPoP proxy needs uvicorn"), + ], + ) + def test_expected_failures_are_reported_without_a_traceback(self, error): + """Actionable failures exit 1 with just their message.""" + result = self._invoke_with_failure(error) + + assert result.exit_code == 1 + assert "Traceback" not in result.output + assert str(error) in result.output + + def test_unexpected_failures_keep_their_traceback(self): + """A bug in the SDK is not disguised as user error.""" + result = self._invoke_with_failure(ZeroDivisionError("bug")) + + assert isinstance(result.exception, ZeroDivisionError) + + +def _jwt_segment(token: str, index: int) -> dict: + """Decode one segment of a JWT without verifying the signature.""" + segment = token.split(".")[index] + padded = segment + "=" * (-len(segment) % 4) + return json.loads(base64.urlsafe_b64decode(padded).decode("utf-8")) + + +def _expected_ath(token: str) -> str: + """Compute the ath claim value for an access token per RFC 9449 4.2.""" + digest = hashlib.sha256(token.encode("ascii")).digest() + return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii") + + +def _run_asgi(app: Any, scope: dict) -> list[dict]: + """Call an ASGI app once with an empty receive queue and collect what it sends.""" + sent: list[dict] = [] + + async def send(message): + sent.append(message) + + async def receive(): + return {"type": "http.request", "body": b"", "more_body": False} + + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(app(scope, receive, send)) + finally: + loop.close() + return sent + + +def _unused_port() -> int: + """Ask the OS for a free port and release it, so a caller can try to bind it.""" + with socket.socket() as probe: + probe.bind(("127.0.0.1", 0)) + return probe.getsockname()[1] + + +@contextmanager +def _running(app: Any, **kwargs) -> Generator[int, None, None]: + """Serve an ASGI app for the duration of the block, yielding its bound port.""" + server, thread, port = start_proxy_server(app, log_level="error", **kwargs) + try: + yield port + finally: + server.should_exit = True + thread.join(timeout=10) + + +class _Upstream: + """ + Echo server standing in for the TES and S3 endpoints. + + Every response is driven by an attribute rather than the request itself, so a + test can ask for a nonce challenge, an error, a slow reply, or a large body on + any path. + """ + + def __init__(self) -> None: + self.requests: list[dict] = [] + self.nonce_challenges_sent = 0 + self.nonce_challenges_to_send = 0 + self.nonce_header = b"server-nonce-1" + self.slow_response_seconds = 0.0 + self.response_megabytes = 0 + self.error_status = 0 + self.error_body = b"" + self.error_content_encoding = b"" + + @property + def last_request(self) -> dict: + """The most recent request the upstream received.""" + return self.requests[-1] + + async def __call__(self, scope, receive, send) -> None: + body = b"" + more_body = True + while more_body: + message = await receive() + body += message.get("body", b"") + more_body = message.get("more_body", False) + + self.requests.append( + { + "method": scope["method"], + "path": scope["path"], + "query": scope["query_string"].decode(), + "headers": {k.decode(): v.decode() for k, v in scope["headers"]}, + "body_length": len(body), + } + ) + + if self.nonce_challenges_to_send > 0: + self.nonce_challenges_to_send -= 1 + self.nonce_challenges_sent += 1 + await self._send_nonce_challenge(send) + return + + if self.error_status: + await self._send_error(send) + return + + if self.slow_response_seconds: + await asyncio.sleep(self.slow_response_seconds) + + if self.response_megabytes: + await self._send_megabytes(send, self.response_megabytes) + return + + await self._send_json(send, {"path": scope["path"], "body_length": len(body)}) + + async def _send_json(self, send, payload: dict) -> None: + """Send a small JSON body.""" + encoded = json.dumps(payload).encode() + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [ + (b"content-type", b"application/json"), + (b"content-length", str(len(encoded)).encode()), + ], + } + ) + await send({"type": "http.response.body", "body": encoded}) + + async def _send_megabytes(self, send, count: int) -> None: + """Stream `count` megabytes back in 1 MiB chunks.""" + chunk = b"z" * (1024 * 1024) + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [ + (b"content-type", b"application/octet-stream"), + (b"content-length", str(count * len(chunk)).encode()), + ], + } + ) + for _ in range(count): + await send({"type": "http.response.body", "body": chunk, "more_body": True}) + await send({"type": "http.response.body", "body": b""}) + + async def _send_error(self, send) -> None: + """Refuse the request the way an upstream reports a problem of its own.""" + headers = [ + (b"content-type", b"application/json"), + (b"content-length", str(len(self.error_body)).encode()), + ] + if self.error_content_encoding: + headers.append((b"content-encoding", self.error_content_encoding)) + + await send( + { + "type": "http.response.start", + "status": self.error_status, + "headers": headers, + } + ) + await send({"type": "http.response.body", "body": self.error_body}) + + async def _send_nonce_challenge(self, send) -> None: + """Reply the way a resource server demands a fresh DPoP nonce.""" + body = b'{"error": "use_dpop_nonce"}' + await send( + { + "type": "http.response.start", + "status": 401, + "headers": [ + (b"content-type", b"application/json"), + (b"content-length", str(len(body)).encode()), + (b"www-authenticate", b'DPoP error="use_dpop_nonce"'), + (b"dpop-nonce", self.nonce_header), + ], + } + ) + await send({"type": "http.response.body", "body": body}) + + +@dataclass +class _Proxy: + """A running AsyncDPoPProxy in front of a running echo upstream.""" + + upstream: _Upstream + upstream_port: int + config: dict + app: AsyncDPoPProxy + url: str + + def request(self, method: str, path: str, **kwargs) -> requests.Response: + """Send a request through the proxy.""" + kwargs.setdefault("timeout", 60) + return requests.request(method, f"{self.url}{path}", **kwargs) + + def get(self, path: str, **kwargs) -> requests.Response: + """GET through the proxy.""" + return self.request("GET", path, **kwargs) + + def put(self, path: str, **kwargs) -> requests.Response: + """PUT through the proxy.""" + return self.request("PUT", path, **kwargs) + + def raw_get_status(self, target: str) -> int: + """ + GET an un-normalized request target and return the status code. + + `requests` resolves `..` in a URL before sending, so a traversal attempt + has to be written onto the wire directly to reach the proxy intact. + """ + port = int(self.url.rsplit(":", 1)[1]) + request = f"GET {target} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\n\r\n" + with socket.create_connection(("127.0.0.1", port), timeout=60) as client: + client.sendall(request.encode()) + status_line = client.recv(64).split(b"\r\n")[0] + return int(status_line.split()[1]) + + def proof(self, index: int = 1) -> dict: + """Decode the DPoP proof the upstream saw last (payload, or header if 0).""" + return _jwt_segment(self.upstream.last_request["headers"]["dpop"], index) + + +def _warnings(records: list[stdlib_logging.LogRecord]) -> list[str]: + """The messages logged at WARNING or above.""" + return [r.getMessage() for r in records if r.levelno >= stdlib_logging.WARNING] + + +def _exchange(ec_key: jwk.Key, **overrides: Any) -> tuple[str, str | None]: + """Exchange an API key for a task token, overriding any default argument.""" + kwargs: dict[str, Any] = { + "key": ec_key, + "token_endpoint": TOKEN_ENDPOINT, + "api_key": "some-api-key", # pragma: allowlist secret + "task_token_type": "WORKFLOW", + } + return exchange_api_key_for_task_token(**{**kwargs, **overrides}) + + +def _refusal(ec_key: jwk.Key, **overrides: Any) -> Gen3AuthError: + """Attempt an exchange expected to fail and return the error it raised.""" + with pytest.raises(Gen3AuthError) as raised: + _exchange(ec_key, **overrides) + return raised.value diff --git a/tests/dpop/test_dpop_nextflow.py b/tests/dpop/test_dpop_nextflow.py new file mode 100644 index 000000000..a5f31e3a2 --- /dev/null +++ b/tests/dpop/test_dpop_nextflow.py @@ -0,0 +1,368 @@ +""" +Test `gen3 nextflow run` (gen3.cli.nextflow, gen3.dpop_nextflow). + +Every test drives the real command through CliRunner with two things stubbed: the +DPoP proxy, so nothing is exchanged for a task token or listens on a socket, and +subprocess.run, so the argv and environment Nextflow would have been given can be +read back without Nextflow being installed. +""" + +from pathlib import Path +from typing import Any, Callable, Iterator +from unittest.mock import MagicMock, patch + +import pytest +from click.testing import CliRunner, Result + +from gen3.auth import Gen3AuthError +from gen3.cli.nextflow import nextflow +from tests.dpop.conftest import COMMONS, TASK_TOKEN + +PROXY_PORT = 61234 + + +class _CompletedProcess: + """Stand-in for subprocess.CompletedProcess carrying an exit code.""" + + def __init__(self, returncode: int) -> None: + self.returncode = returncode + + +@pytest.fixture +def fake_proxy() -> Iterator[MagicMock]: + """Patch dpop_proxy_context so no token exchange or proxy startup happens.""" + with patch("gen3.dpop_nextflow.dpop_proxy_context") as proxy_context: + proxy_context.return_value.__enter__.return_value = (TASK_TOKEN, PROXY_PORT) + yield proxy_context + + +@pytest.fixture +def nextflow_process() -> Iterator[MagicMock]: + """ + Patch the subprocess.run that starts Nextflow. + + A test that needs a failed run sets `return_value` or `side_effect` on the mock + before invoking the command. + """ + with patch("subprocess.run", return_value=_CompletedProcess(0)) as run: + yield run + + +@pytest.fixture +def generated_config(nextflow_process) -> dict[str, Any]: + """ + Capture the config `nextflow run` was given. + + The file is deleted when the run ends, so it has to be read while the child + process is notionally running. + """ + captured: dict[str, Any] = {} + + def capture(*args: Any, **kwargs: Any) -> _CompletedProcess: + argv = args[0] + captured["path"] = Path(argv[argv.index("-c") + 1]) + captured["contents"] = captured["path"].read_text() + return _CompletedProcess(0) + + nextflow_process.side_effect = capture + return captured + + +@pytest.fixture +def auth_factory() -> MagicMock: + """ + The lazy auth factory the root group installs, naming the fake commons. + + Built per test: a factory shared between them would carry call history from + one into the next, so nothing could be asserted about who asked for auth. + """ + factory = MagicMock() + factory.get.return_value.endpoint = COMMONS + return factory + + +@pytest.fixture +def run_cli( + auth_factory, fake_proxy, nextflow_process +) -> Callable[[list[str]], Result]: + """ + Run `gen3 nextflow run` with the arguments a user would type. + + Depends on both stubs so that no test can reach a real commons or a real + `nextflow`, whether or not it asserts on them. + """ + + def invoke(args: list[str]) -> Result: + return CliRunner().invoke( + nextflow, ["run"] + args, obj={"auth_factory": auth_factory} + ) + + return invoke + + +class TestNextflowInvocation: + """What the Nextflow child process is given, and what its exit means.""" + + def test_nextflow_args_are_forwarded(self, run_cli, nextflow_process): + """Nextflow's own flags are appended to `nextflow run`, not rejected by Click.""" + run_cli(["main.nf", "-profile", "gen3", "-resume"]) + + argv = nextflow_process.call_args.args[0] + assert argv[:6] == [ + "nextflow", + "run", + "main.nf", + "-profile", + "gen3", + "-resume", + ] + assert argv[-2] == "-c" + + def test_a_double_dash_hands_colliding_params_to_nextflow( + self, run_cli, nextflow_process, fake_proxy + ): + """`--port` is a plausible pipeline param, so it has to be passable through.""" + run_cli(["main.nf", "--", "--port", "8080"]) + + assert fake_proxy.call_args.kwargs["port"] is None + assert _pipeline_args(nextflow_process) == ["main.nf", "--port", "8080"] + + def test_token_and_port_are_exported_to_nextflow(self, run_cli, nextflow_process): + """The child environment carries the task token and the proxy port.""" + run_cli(["main.nf"]) + + env = nextflow_process.call_args.kwargs["env"] + assert env["GEN3_DPOP_BOUND_TASK_TOKEN"] == TASK_TOKEN + assert env["GEN3_DPOP_PROXY_PORT"] == str(PROXY_PORT) + + def test_parent_environment_is_preserved( + self, run_cli, nextflow_process, monkeypatch + ): + """Existing environment variables still reach Nextflow.""" + monkeypatch.setenv("NXF_HOME", "/tmp/nxf-home") + + run_cli(["main.nf"]) + + assert nextflow_process.call_args.kwargs["env"]["NXF_HOME"] == "/tmp/nxf-home" + + @pytest.mark.parametrize("return_code", [0, 1, 3, 137]) + def test_the_pipeline_exit_code_becomes_the_cli_exit_code( + self, run_cli, nextflow_process, return_code + ): + """Click discards return values, so a failed pipeline has to be exited on.""" + nextflow_process.return_value = _CompletedProcess(return_code) + + assert run_cli(["main.nf"]).exit_code == return_code + + def test_a_child_that_cannot_run_still_stops_the_proxy( + self, run_cli, nextflow_process, fake_proxy + ): + """The proxy holds a task token, so it must not outlive a failed run.""" + nextflow_process.side_effect = OSError("boom") + + result = run_cli(["main.nf"]) + + assert isinstance(result.exception, OSError) + fake_proxy.return_value.__exit__.assert_called_once() + + +class TestGeneratedConfig: + """The override that redirects a commons-facing config to the proxy.""" + + def test_it_is_the_last_config_so_it_wins(self, run_cli, nextflow_process): + """Nextflow lets the last -c win, so ours has to come after the user's.""" + run_cli(["main.nf", "-c", "mine.config"]) + + argv = nextflow_process.call_args.args[0] + assert argv[:5] == ["nextflow", "run", "main.nf", "-c", "mine.config"] + assert argv[-2] == "-c" + assert argv[-1].endswith("gen3-dpop.config") + + def test_it_redirects_both_endpoints_to_the_proxy(self, run_cli, generated_config): + """The whole point: traffic aimed at the commons goes to the proxy instead.""" + run_cli(["main.nf"]) + + contents = generated_config["contents"] + proxy = f"http://127.0.0.1:{PROXY_PORT}" + assert f"endpoint = '{proxy}/ga4gh/tes'" in contents + assert f"endpoint = '{proxy}/s3'" in contents + assert "s3PathStyleAccess = true" in contents + + @pytest.mark.parametrize( + "owned_by_the_user", ["workDir", "region", "executor", "plugins"] + ) + def test_it_overrides_nothing_else( + self, run_cli, generated_config, owned_by_the_user + ): + """A setting the proxy does not force must be left to the user's config.""" + run_cli(["main.nf"]) + + # The header comment names these to explain what is left alone, so only the + # config statements themselves can be checked for them. + statements = "\n".join( + line + for line in generated_config["contents"].splitlines() + if not line.strip().startswith("//") + ) + assert owned_by_the_user not in statements + + def test_the_upstreams_are_named_so_users_can_see_where_traffic_goes( + self, run_cli, generated_config + ): + """127.0.0.1 endpoints are alarming without saying what is behind them.""" + run_cli(["main.nf"]) + + assert f"{COMMONS}/ga4gh/tes" in generated_config["contents"] + assert f"{COMMONS}/workflows/s3" in generated_config["contents"] + + def test_the_token_is_not_written_to_the_file(self, run_cli, generated_config): + """A temp file holding a bearer-equivalent credential is world-readable.""" + run_cli(["main.nf"]) + + assert TASK_TOKEN not in generated_config["contents"] + assert "env('GEN3_DPOP_BOUND_TASK_TOKEN')" in generated_config["contents"] + + def test_the_file_is_cleaned_up_after_the_run(self, run_cli, generated_config): + """The token lives in the environment, but the config still goes away.""" + run_cli(["main.nf"]) + + assert not generated_config["path"].exists() + + def test_no_config_is_passed_when_it_is_turned_off(self, run_cli, nextflow_process): + """--no-generated-config means the user's config is the only one.""" + run_cli(["main.nf", "--no-generated-config"]) + + assert "-c" not in nextflow_process.call_args.args[0] + + +class TestProxyOptions: + """The options the command owns rather than forwarding to Nextflow.""" + + @pytest.mark.parametrize( + "args,expected", + [ + # No --port means "let the OS pick", so it arrives as None not 0. + pytest.param( + [], + { + "port": None, + "task_token_type": "WORKFLOW", + "task_token_expiration": None, + "tes_endpoint": None, + "s3_endpoint": None, + }, + id="defaults", + ), + pytest.param(["--port", "8123"], {"port": 8123}, id="port"), + pytest.param( + ["--task-token-type", "TASK"], {"task_token_type": "TASK"}, id="type" + ), + pytest.param( + ["--task-token-expiration", "1800"], + {"task_token_expiration": 1800}, + id="expiration", + ), + pytest.param( + ["--tes-endpoint", f"{COMMONS}/ga4gh/tes"], + {"tes_endpoint": f"{COMMONS}/ga4gh/tes"}, + id="tes_endpoint", + ), + pytest.param( + ["--s3-endpoint", f"{COMMONS}/workflows/s3"], + {"s3_endpoint": f"{COMMONS}/workflows/s3"}, + id="s3_endpoint", + ), + ], + ) + def test_proxy_options_are_forwarded( + self, run_cli, fake_proxy, nextflow_process, args, expected + ): + """Every proxy option reaches the proxy, and none of them reach Nextflow.""" + result = run_cli(["main.nf"] + args) + + assert result.exit_code == 0 + assert expected.items() <= fake_proxy.call_args.kwargs.items() + assert _pipeline_args(nextflow_process) == ["main.nf"] + + def test_port_can_be_pinned_with_an_environment_variable( + self, run_cli, fake_proxy, monkeypatch + ): + """The same variable the proxy exports also pins the port on the way in.""" + monkeypatch.setenv("GEN3_DPOP_PROXY_PORT", "8000") + + result = run_cli(["main.nf"]) + + assert result.exit_code == 0 + assert fake_proxy.call_args.kwargs["port"] == 8000 + + def test_credentials_come_from_the_root_group( + self, run_cli, auth_factory, fake_proxy + ): + """Auth is resolved through the lazy factory the root group installs.""" + result = run_cli(["main.nf"]) + + assert result.exit_code == 0 + auth_factory.get.assert_called_once_with() + assert fake_proxy.call_args.kwargs["auth"] is auth_factory.get.return_value + + def test_an_endpoint_off_the_commons_fails_before_the_token_exchange( + self, run_cli, fake_proxy, nextflow_process + ): + """Endpoints are resolved first, so a bad one costs no credentials.""" + result = run_cli( + ["main.nf", "--tes-endpoint", "https://other.example.com/ga4gh/tes"] + ) + + assert result.exit_code == 1 + assert not fake_proxy.called + assert not nextflow_process.called + + +class TestErrorReporting: + """How a failed run is reported to whoever typed the command.""" + + @pytest.mark.parametrize( + "failing_fixture,error", + [ + pytest.param( + "fake_proxy", + Gen3AuthError("the commons explained why"), + id="token_exchange", + ), + pytest.param( + "nextflow_process", + FileNotFoundError("nextflow is not installed"), + id="missing_executable", + ), + ], + ) + def test_expected_failures_are_reported_without_a_traceback( + self, request, run_cli, failing_fixture, error + ): + """An actionable failure exits 1 with its own message and no traceback.""" + request.getfixturevalue(failing_fixture).side_effect = error + + result = run_cli(["main.nf"]) + + assert result.exit_code == 1 + assert "Traceback" not in result.output + assert str(error) in result.output + + def test_unexpected_failures_keep_their_traceback(self, run_cli, nextflow_process): + """A bug in the SDK is not disguised as user error.""" + nextflow_process.side_effect = ZeroDivisionError("bug") + + assert isinstance(run_cli(["main.nf"]).exception, ZeroDivisionError) + + def test_help_does_not_require_credentials(self): + """`--help` works without any Gen3 credentials configured.""" + result = CliRunner().invoke(nextflow, ["run", "--help"]) + + assert result.exit_code == 0 + assert "Usage:" in result.output + + +def _pipeline_args(nextflow_process: MagicMock) -> list[str]: + """The arguments Nextflow got, without `nextflow run` or the generated `-c`.""" + argv = nextflow_process.call_args.args[0] + return argv[2:-2] diff --git a/tests/fake_indexd.py b/tests/fake_indexd.py new file mode 100644 index 000000000..930effd2c --- /dev/null +++ b/tests/fake_indexd.py @@ -0,0 +1,410 @@ +""" +An in-memory stand-in for the indexd HTTP API, for tests that need a live index. + +One store, served either by patching the SDK's HTTP clients - `requests` (Gen3Index and +indexclient) and `httpx` (drsclient) - or over a real socket for code that leaves the +process and so cannot see those patches. + +Only the endpoints and query parameters the tests exercise are implemented. Unknown +query parameters are ignored rather than filtered on, so a test that needs a new filter +has to add it here. +""" + +import json +import threading +from collections.abc import Generator +from contextlib import ExitStack, contextmanager +from copy import deepcopy +from datetime import datetime, timezone +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from types import MappingProxyType +from typing import Any +from unittest.mock import patch +from urllib.parse import parse_qs, urlparse +from uuid import uuid4 + +import httpx +import requests_mock +from drsclient import client as drsclient_module + +# Gen3Index reads "http://localhost" as indexd deployed on its own rather than behind a +# commons, and drops the `/index` service location. Tools under test build their own +# Gen3Index from this URL, so it has to keep that property. +BASE_URL = "http://localhost:8001" + +DEFAULT_PAGE_SIZE = 100 + +# indexd's own test configuration mints GUIDs as "", and a caller +# that lets indexd choose the GUID can assert on that prefix. +DEFAULT_PREFIX = "testprefix/" + +# Read-only, and copied per record +_RECORD_DEFAULTS = MappingProxyType( + { + "acl": [], + "authz": [], + "content_created_date": None, + "content_updated_date": None, + "description": None, + "file_name": None, + "form": "object", + "hashes": {}, + "metadata": {}, + "size": None, + "uploader": None, + "urls": [], + "urls_metadata": {}, + "version": None, + } +) + + +class FakeIndexd: + """ + The store, its request handler, and the patches that route clients to it. + + Attributes: + baseurl (str): The URL clients should be pointed at. + records (dict): Index records by did, in creation order. + bundles (dict): Bundle records by bundle_id, in creation order. + """ + + def __init__(self, baseurl: str = BASE_URL) -> None: + self.baseurl = baseurl + self.records: dict[str, dict] = {} + self.bundles: dict[str, dict] = {} + + @contextmanager + def serving_in_process(self) -> Generator["FakeIndexd", None, None]: + """ + Route this process's requests and httpx calls to this store. + + Yields: + FakeIndexd: This instance, once the patches are in place. + """ + with ExitStack() as stack: + mocker = stack.enter_context(requests_mock.Mocker()) + mocker.register_uri( + requests_mock.ANY, requests_mock.ANY, text=self._serve_requests + ) + # drsclient builds its own httpx client per call, so the class it + # instantiates is the only place a transport can be injected. + stack.enter_context( + patch("drsclient.client.SyncClient", self._mocked_httpx_client()) + ) + yield self + + @contextmanager + def serving_over_http(self) -> Generator["FakeIndexd", None, None]: + """ + Serve this store over a real socket, and point `baseurl` at it. + + For code that leaves the process: `download_object_manifest` fans out with + `asyncio.create_subprocess_shell`, and the children's HTTP traffic cannot be + intercepted by any in-process patch. + + Yields: + FakeIndexd: This instance, once the server is accepting connections. + """ + server = ThreadingHTTPServer(("127.0.0.1", 0), _handler_for(self)) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + + # Gen3Index only drops the `/index` service location for "http://localhost". + self.baseurl = f"http://localhost:{server.server_address[1]}" + try: + yield self + finally: + server.shutdown() + server.server_close() + thread.join() + + def handle( + self, method: str, url: str, body: bytes | None + ) -> tuple[int, Any, bool]: + """ + Serve one request. + + Args: + method (str): HTTP method. + url (str): Full request URL. + body (bytes | None): Request body, if any. + + Returns: + tuple[int, Any, bool]: Status code, payload, and whether the payload is + already text rather than something to serialize as JSON. + """ + parsed = urlparse(url) + path = parsed.path.rstrip("/") or "/" + query = {key: values[-1] for key, values in parse_qs(parsed.query).items()} + payload = json.loads(body) if body else {} + + if path == "/_status": + return 200, "Healthy", True + if path == "/_version": + return 200, {"version": "fake", "commit": "0" * 40}, False + if path == "/_stats": + return 200, self._stats(), False + if path == "/urls": + return 200, self._urls(query), False + if path == "/bulk/documents": + return ( + 200, + [self.records[did] for did in payload if did in self.records], + False, + ) + if path == "/index": + if method == "POST": + return self._create(payload) + return 200, self._list(query), False + if path == "/bundle": + if method == "POST": + return self._create_bundle(payload) + return 200, {"records": list(self.bundles.values())}, False + if path.startswith("/ga4gh/drs/v1/objects/"): + return self._fetch(path[len("/ga4gh/drs/v1/objects/") :]) + if path.startswith("/index/"): + return self._index_route(method, path[len("/index/") :], query, payload) + if path.startswith("/bundle/"): + return self._fetch(path[len("/bundle/") :]) + + # Anything else is either a global (dist resolution) lookup or a route this + # stub does not implement; the two are told apart by whether it resolves. + return self._fetch(path.lstrip("/")) + + def _index_route( + self, method: str, rest: str, query: dict, payload: Any + ) -> tuple[int, Any, bool]: + """Serve the `/index/...` routes, whose dids may themselves contain a `/`.""" + if rest == "blank": + return self._create_blank(payload) + if rest.startswith("blank/"): + return self._update(rest[len("blank/") :], payload) + if rest.endswith("/versions"): + versions = self._versions(rest[: -len("/versions")]) + return 200, {str(i): record for i, record in enumerate(versions)}, False + if rest.endswith("/latest"): + return self._latest(rest[: -len("/latest")], query) + + if method == "POST": + return self._add_version(rest, payload) + if method == "PUT": + return self._update(rest, payload) + if method == "DELETE": + if rest not in self.records: + return 404, {"error": "no record found"}, False + del self.records[rest] + return 200, "", True + return self._fetch(rest) + + def _fetch(self, guid: str) -> tuple[int, Any, bool]: + """Look a guid up as a record did, a bundle id, or a baseid.""" + if guid in self.records: + return 200, self.records[guid], False + if guid in self.bundles: + return 200, self.bundles[guid], False + + by_baseid = [r for r in self.records.values() if r["baseid"] == guid] + if by_baseid: + return 200, by_baseid[-1], False + return 404, {"error": "no record found"}, False + + def _create(self, payload: dict) -> tuple[int, Any, bool]: + """Create a record, keeping any did or baseid the caller chose.""" + did = payload.get("did") or f"{DEFAULT_PREFIX}{uuid4()}" + if did in self.records: + return 409, {"error": f"{did} already exists"}, False + + record = {key: deepcopy(value) for key, value in _RECORD_DEFAULTS.items()} + record.update({k: v for k, v in payload.items() if v is not None}) + now = datetime.now(timezone.utc).isoformat() + record.update( + { + "did": did, + "baseid": payload.get("baseid") or str(uuid4()), + "rev": uuid4().hex[:8], + "created_date": now, + "updated_date": now, + } + ) + self.records[did] = record + return 200, self._identity(record), False + + def _create_blank(self, payload: dict) -> tuple[int, Any, bool]: + """A blank record carries an uploader and nothing to download yet.""" + return self._create( + {"uploader": payload.get("uploader"), "file_name": payload.get("file_name")} + ) + + def _add_version(self, did: str, payload: dict) -> tuple[int, Any, bool]: + """Add a version of an existing record, which shares its baseid.""" + if did not in self.records: + return 404, {"error": "no record found"}, False + + new_version = dict(payload) + new_version.pop("rev", None) + new_version["baseid"] = self.records[did]["baseid"] + if new_version.get("did") == did: + new_version.pop("did") + return self._create(new_version) + + def _update(self, did: str, payload: dict) -> tuple[int, Any, bool]: + """Apply an update and bump the revision, as a write to indexd would.""" + if did not in self.records: + return 404, {"error": "no record found"}, False + + record = self.records[did] + record.update({k: v for k, v in payload.items() if k not in ("did", "rev")}) + record["rev"] = uuid4().hex[:8] + record["updated_date"] = datetime.now(timezone.utc).isoformat() + return 200, self._identity(record), False + + def _versions(self, did: str) -> list[dict]: + """Every record sharing a baseid with `did`, in creation order.""" + if did not in self.records: + return [] + baseid = self.records[did]["baseid"] + return [r for r in self.records.values() if r["baseid"] == baseid] + + def _latest(self, did: str, query: dict) -> tuple[int, Any, bool]: + """The newest version of a record, optionally skipping unversioned ones.""" + versions = self._versions(did) + if query.get("has_version") == "true": + versions = [r for r in versions if r["version"]] + if not versions: + return 404, {"error": "no record found"}, False + return 200, versions[-1], False + + def _list(self, query: dict) -> dict: + """Serve `GET /index` with the filters and paging the tests use.""" + limit = int(query.get("limit", DEFAULT_PAGE_SIZE)) + records = self._filtered(query) + + if "start" in query: + dids = [r["did"] for r in records] + after = dids.index(query["start"]) + 1 if query["start"] in dids else 0 + records = records[after:] + elif "page" in query: + page = int(query["page"]) + records = records[page * limit : (page + 1) * limit] + + return {"records": records[:limit], "limit": limit, "start": query.get("start")} + + def _urls(self, query: dict) -> dict: + """`GET /urls` answers with the urls of the matching records, keyed by did.""" + return {r["did"]: r["urls"] for r in self._filtered(query)} + + def _filtered(self, query: dict) -> list[dict]: + """The records matching `hash`, `size` and `ids`; other filters are ignored.""" + records = list(self.records.values()) + + if "ids" in query: + wanted = query["ids"].split(",") + records = [r for r in records if r["did"] in wanted] + if "size" in query: + records = [r for r in records if str(r["size"]) == query["size"]] + if "hash" in query: + algorithm, _, value = query["hash"].partition(":") + records = [r for r in records if r["hashes"].get(algorithm) == value] + return records + + def _stats(self) -> dict: + """`GET /_stats`, which paging callers read the record count from.""" + return { + "fileCount": len(self.records), + "totalFileSize": sum(r["size"] or 0 for r in self.records.values()), + } + + def _create_bundle(self, payload: dict) -> tuple[int, Any, bool]: + """Store a bundle, keeping the checksums the caller supplied.""" + bundle_id = payload.get("bundle_id") or str(uuid4()) + bundle = dict(payload) + bundle.update( + { + "id": bundle_id, + "bundle_id": bundle_id, + "name": payload.get("name") or bundle_id, + "checksums": payload.get("checksums", []), + "size": payload.get("size", 0), + "created_time": datetime.now(timezone.utc).isoformat(), + } + ) + self.bundles[bundle_id] = bundle + return 200, {"bundle_id": bundle_id, "name": bundle["name"]}, False + + @staticmethod + def _identity(record: dict) -> dict: + """What indexd echoes back from a write: enough to fetch the record again.""" + return {key: record[key] for key in ("did", "rev", "baseid")} + + def _serve_requests(self, request: Any, context: Any) -> str: + """requests_mock callback: serve `request` and return the body as text.""" + status, payload, is_text = self.handle( + request.method, request.url, request.body + ) + context.status_code = status + if is_text: + return payload + context.headers["Content-Type"] = "application/json" + return json.dumps(payload) + + def _serve_httpx(self, request: httpx.Request) -> httpx.Response: + """httpx.MockTransport handler, for drsclient.""" + status, payload, is_text = self.handle( + request.method, str(request.url), request.content + ) + if is_text: + return httpx.Response(status, text=payload) + return httpx.Response(status, json=payload) + + def _mocked_httpx_client(self) -> type[drsclient_module.SyncClient]: + """A drsclient SyncClient subclass that talks to this store.""" + transport = httpx.MockTransport(self._serve_httpx) + + class MockedSyncClient(drsclient_module.SyncClient): + def __init__(self, *args, **kwargs) -> None: + kwargs["transport"] = transport + super().__init__(*args, **kwargs) + + return MockedSyncClient + + +def _handler_for(fake: FakeIndexd) -> type[BaseHTTPRequestHandler]: + """ + Build a BaseHTTPRequestHandler serving `fake`. + + Args: + fake (FakeIndexd): The store to serve. + + Returns: + type[BaseHTTPRequestHandler]: A handler class for ThreadingHTTPServer. + """ + + class Handler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def do_GET(self) -> None: + self._serve("GET") + + def do_POST(self) -> None: + self._serve("POST") + + def log_message(self, format: str, *args: Any) -> None: + """Keep the default per-request logging out of the test output.""" + pass + + def _serve(self, method: str) -> None: + length = int(self.headers.get("Content-Length") or 0) + body = self.rfile.read(length) if length else None + status, payload, is_text = fake.handle(method, self.path, body) + + content = (payload if is_text else json.dumps(payload)).encode() + self.send_response(status) + self.send_header( + "Content-Type", "text/plain" if is_text else "application/json" + ) + self.send_header("Content-Length", str(len(content))) + self.end_headers() + self.wfile.write(content) + + return Handler diff --git a/tests/test_import.py b/tests/test_import.py index 07b5e6dfb..909062e34 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -6,11 +6,6 @@ def test_import(): import gen3 -def test_cdisutilstest(): - - import cdisutilstest - - def test_indexclient(): import indexclient diff --git a/tests/test_manifests.py b/tests/test_manifests.py index 2e29adb04..2f4aa94dd 100644 --- a/tests/test_manifests.py +++ b/tests/test_manifests.py @@ -17,6 +17,10 @@ CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) +# Two test files attach a FileHandler to this path and delete it on teardown, so a +# name shared across xdist workers lets one truncate another's log mid-test. +LOG_FILE = f"gen3tests-{os.environ.get('PYTEST_XDIST_WORKER', 'main')}.logs" + @pytest.fixture(autouse=True) def set_log_level_to_error(): @@ -25,9 +29,9 @@ def set_log_level_to_error(): """ logging.setLevel(default_logging.ERROR) - if os.path.exists("gen3tests.logs"): - os.remove("gen3tests.logs") - logfile_handler = default_logging.FileHandler("gen3tests.logs") + if os.path.exists(LOG_FILE): + os.remove(LOG_FILE) + logfile_handler = default_logging.FileHandler(LOG_FILE) logfile_handler.setFormatter(default_logging.Formatter(LOG_FORMAT)) logging.addHandler(logfile_handler) yield @@ -51,11 +55,11 @@ def read(self): self.logs += line return self.logs - yield Logfile(filename="gen3tests.logs") + yield Logfile(filename=LOG_FILE) # cleanup after each use - if os.path.exists("gen3tests.logs"): - os.remove("gen3tests.logs") + if os.path.exists(LOG_FILE): + os.remove(LOG_FILE) @patch("gen3.tools.indexing.verify_manifest.Gen3Index") @@ -124,11 +128,11 @@ def test_verify_manifest(mock_index): assert "no_record" in logs["dg.TEST/9c205cd7-c399-4503-9f49-5647188bde66"] -def test_download_manifest(monkeypatch, gen3_index): +def test_download_manifest(monkeypatch, gen3_index_over_http): """ Test that dowload manifest generates a file with expected content. """ - rec1 = gen3_index.create_record( + rec1 = gen3_index_over_http.create_record( did="dg.TEST/f2a39f98-6ae1-48a5-8d48-825a0c52a22b", hashes={"md5": "a1234567891234567890123456789012"}, size=123, @@ -136,7 +140,7 @@ def test_download_manifest(monkeypatch, gen3_index): authz=["/programs/DEV/projects/test"], urls=["s3://testaws/aws/test.txt", "gs://test/test.txt"], ) - rec2 = gen3_index.create_record( + rec2 = gen3_index_over_http.create_record( did="dg.TEST/1e9d3103-cbe2-4c39-917c-b3abad4750d2", hashes={"md5": "b1234567891234567890123456789012"}, size=234, @@ -145,7 +149,7 @@ def test_download_manifest(monkeypatch, gen3_index): urls=["gs://test/test.txt"], file_name="test.txt", ) - rec3 = gen3_index.create_record( + rec3 = gen3_index_over_http.create_record( did="dg.TEST/ed8f4658-6acd-4f96-9dd8-3709890c959e", hashes={"md5": "e1234567891234567890123456789012"}, size=345, @@ -154,7 +158,7 @@ def test_download_manifest(monkeypatch, gen3_index): urls=["gs://test/test3.txt"], ) # record with space - rec4 = gen3_index.create_record( + rec4 = gen3_index_over_http.create_record( did="dg.TEST/a802e27d-4a5b-42e3-92b0-ba19e81b9dce", hashes={"md5": "f1234567891234567890123456789012"}, size=345, @@ -169,7 +173,7 @@ def test_download_manifest(monkeypatch, gen3_index): loop = get_or_create_event_loop_for_thread() loop.run_until_complete( async_download_object_manifest( - "http://localhost:8001", + gen3_index_over_http.client.url, output_filename="tests/outputs/object-manifest.csv", num_processes=1, ) @@ -241,7 +245,7 @@ def test_download_manifest(monkeypatch, gen3_index): ).get("file_name") -def test_download_manifest_with_input_manifest(monkeypatch, gen3_index): +def test_download_manifest_with_input_manifest(monkeypatch, gen3_index_over_http): """ Test that dowload manifest generates a file with expected content when provided an initial input manifest. @@ -257,7 +261,7 @@ def test_download_manifest_with_input_manifest(monkeypatch, gen3_index): ] ) - rec1 = gen3_index.create_record( + rec1 = gen3_index_over_http.create_record( did="dg.TEST/f2a39f98-6ae1-48a5-8d48-825a0c52a22b", hashes={"md5": "a1234567891234567890123456789012"}, size=123, @@ -265,7 +269,7 @@ def test_download_manifest_with_input_manifest(monkeypatch, gen3_index): authz=["/programs/DEV/projects/test"], urls=["s3://testaws/aws/test.txt", "gs://test/test.txt"], ) - rec2 = gen3_index.create_record( + rec2 = gen3_index_over_http.create_record( did="dg.TEST/1e9d3103-cbe2-4c39-917c-b3abad4750d2", hashes={"md5": "b1234567891234567890123456789012"}, size=234, @@ -274,7 +278,7 @@ def test_download_manifest_with_input_manifest(monkeypatch, gen3_index): urls=["gs://test/test.txt"], file_name="test.txt", ) - rec3 = gen3_index.create_record( + rec3 = gen3_index_over_http.create_record( did="dg.TEST/ed8f4658-6acd-4f96-9dd8-3709890c959e", hashes={"md5": "e1234567891234567890123456789012"}, size=345, @@ -283,7 +287,7 @@ def test_download_manifest_with_input_manifest(monkeypatch, gen3_index): urls=["gs://test/test3.txt"], ) # record with space - rec4 = gen3_index.create_record( + rec4 = gen3_index_over_http.create_record( did="dg.TEST/a802e27d-4a5b-42e3-92b0-ba19e81b9dce", hashes={"md5": "f1234567891234567890123456789012"}, size=345, @@ -297,7 +301,7 @@ def test_download_manifest_with_input_manifest(monkeypatch, gen3_index): loop = get_or_create_event_loop_for_thread() loop.run_until_complete( async_download_object_manifest( - "http://localhost:8001", + gen3_index_over_http.client.url, output_filename="tests/outputs/object-manifest.csv", num_processes=1, input_manifest=file_path, diff --git a/tests/validate_manifest_format/test_is_valid_manifest_format.py b/tests/validate_manifest_format/test_is_valid_manifest_format.py index 99bb7fb36..acc8194cb 100644 --- a/tests/validate_manifest_format/test_is_valid_manifest_format.py +++ b/tests/validate_manifest_format/test_is_valid_manifest_format.py @@ -6,6 +6,10 @@ from gen3.tools.indexing import is_valid_manifest_format from gen3.tools.utils import Columns +# Two test files attach a FileHandler to this path and delete it on teardown, so a +# name shared across xdist workers lets one truncate another's log mid-test. +LOG_FILE = f"gen3tests-{os.environ.get('PYTEST_XDIST_WORKER', 'main')}.logs" + @pytest.fixture(autouse=True) def set_log_level_to_warning(): @@ -14,9 +18,9 @@ def set_log_level_to_warning(): """ logging.setLevel(default_logging.WARNING) - if os.path.exists("gen3tests.logs"): - os.remove("gen3tests.logs") - logfile_handler = default_logging.FileHandler("gen3tests.logs") + if os.path.exists(LOG_FILE): + os.remove(LOG_FILE) + logfile_handler = default_logging.FileHandler(LOG_FILE) logfile_handler.setFormatter(default_logging.Formatter(LOG_FORMAT)) logging.addHandler(logfile_handler) yield @@ -40,11 +44,11 @@ def read(self): self.logs += line return self.logs - yield Logfile(filename="gen3tests.logs") + yield Logfile(filename=LOG_FILE) # cleanup after each use - if os.path.exists("gen3tests.logs"): - os.remove("gen3tests.logs") + if os.path.exists(LOG_FILE): + os.remove(LOG_FILE) def test_is_valid_manifest_format_with_no_errors(logfile): From a8c56a2dc2754eafc186a451256921c44cf121e4 Mon Sep 17 00:00:00 2001 From: Avantol13 Date: Thu, 20 Aug 2026 21:06:40 +0000 Subject: [PATCH 4/7] Apply automatic documentation changes --- docs/_build/html/_modules/gen3/auth.html | 354 ++++++------ docs/_build/html/_modules/gen3/file.html | 104 ++-- docs/_build/html/_modules/gen3/index.html | 426 +++++++-------- docs/_build/html/_modules/gen3/jobs.html | 120 ++-- docs/_build/html/_modules/gen3/metadata.html | 428 +++++++-------- docs/_build/html/_modules/gen3/object.html | 32 +- docs/_build/html/_modules/gen3/query.html | 96 ++-- .../_build/html/_modules/gen3/submission.html | 378 ++++++------- .../gen3/tools/download/drs_download.html | 512 +++++++++--------- .../tools/indexing/download_manifest.html | 242 ++++----- .../gen3/tools/indexing/index_manifest.html | 258 ++++----- .../gen3/tools/indexing/verify_manifest.html | 238 ++++---- .../gen3/tools/metadata/ingest_manifest.html | 206 +++---- docs/_build/html/_modules/gen3/wss.html | 98 ++-- docs/_build/html/auth.html | 8 +- docs/_build/html/file.html | 2 +- docs/_build/html/indexing.html | 2 +- docs/_build/html/jobs.html | 2 +- docs/_build/html/metadata.html | 2 +- docs/_build/html/object.html | 2 +- docs/_build/html/query.html | 28 +- docs/_build/html/submission.html | 36 +- docs/_build/html/tools/drs_pull.html | 8 +- docs/_build/html/wss.html | 2 +- 24 files changed, 1792 insertions(+), 1792 deletions(-) diff --git a/docs/_build/html/_modules/gen3/auth.html b/docs/_build/html/_modules/gen3/auth.html index b0774ebaf..c4427e760 100644 --- a/docs/_build/html/_modules/gen3/auth.html +++ b/docs/_build/html/_modules/gen3/auth.html @@ -58,35 +58,35 @@

    Source code for gen3.auth

     
     
     def decode_token(token_str):
    -    """
    -    jq -r '.api_key' < ~/.gen3/qa-covid19.planx-pla.net.json | awk -F . '{ print $2 }' | base64 --decode | jq -r .
    -    """
    -    tokenParts = token_str.split(".")
    +    """
    +    jq -r '.api_key' < ~/.gen3/qa-covid19.planx-pla.net.json | awk -F . '{ print $2 }' | base64 --decode | jq -r .
    +    """
    +    tokenParts = token_str.split(".")
         if len(tokenParts) < 3:
    -        raise Exception("Invalid JWT. Could not split into parts.")
    -    padding = "===="
    +        raise Exception("Invalid JWT. Could not split into parts.")
    +    padding = "===="
         infoStr = tokenParts[1] + padding[0 : len(tokenParts[1]) % 4]
         jsonStr = base64.urlsafe_b64decode(infoStr)
         return json.loads(jsonStr)
     
     
     def endpoint_from_token(token_str):
    -    """
    -    Extract the endpoint from a JWT issue ("iss" property)
    -    """
    +    """
    +    Extract the endpoint from a JWT issue ("iss" property)
    +    """
         info = decode_token(token_str)
    -    urlparts = urlparse(info["iss"])
    -    endpoint = urlparts.scheme + "://" + urlparts.hostname
    +    urlparts = urlparse(info["iss"])
    +    endpoint = urlparts.scheme + "://" + urlparts.hostname
         if urlparts.port:
    -        endpoint += ":" + str(urlparts.port)
    +        endpoint += ":" + str(urlparts.port)
         return remove_trailing_whitespace_and_slashes_in_url(endpoint)
     
     
     def _handle_access_token_response(resp, token_key):
    -    """
    +    """
         Shared helper for both get_access_token_with_key and get_access_token_from_wts
    -    """
    -    err_msg = "Failed to get an access token from {}:\n{}"
    +    """
    +    err_msg = "Failed to get an access token from {}:\n{}"
         if resp.status_code != 200:
             raise Gen3AuthError(err_msg.format(resp.url, resp.text))
         try:
    @@ -99,64 +99,64 @@ 

    Source code for gen3.auth

     
     
     def get_access_token_with_key(api_key):
    -    """
    +    """
         Try to fetch an access token given the api key
    -    """
    -    endpoint = endpoint_from_token(api_key["api_key"])
    +    """
    +    endpoint = endpoint_from_token(api_key["api_key"])
         # attempt to get a token from Fence
    -    auth_url = "{}/user/credentials/api/access_token".format(endpoint)
    +    auth_url = "{}/user/credentials/api/access_token".format(endpoint)
         resp = requests.post(auth_url, json=api_key)
    -    token_key = "access_token"
    +    token_key = "access_token"
         return _handle_access_token_response(resp, token_key)
     
     
     def get_access_token_with_client_credentials(endpoint, client_credentials, scopes):
    -    """
    +    """
         Try to get an access token from Fence using client credentials
     
         Args:
             endpoint (str): URL of the Gen3 instance to get an access token for
             client_credentials ((str, str) tuple): (client ID, client secret) tuple
             scopes (str): space-delimited list of scopes to request
    -    """
    +    """
         if not endpoint:
    -        raise ValueError("'endpoint' must be specified when using client credentials")
    -    url = f"{endpoint}/user/oauth2/token?grant_type=client_credentials&scope={scopes}"
    +        raise ValueError("'endpoint' must be specified when using client credentials")
    +    url = f"{endpoint}/user/oauth2/token?grant_type=client_credentials&scope={scopes}"
         resp = requests.post(url, auth=client_credentials)
    -    return _handle_access_token_response(resp, "access_token")
    +    return _handle_access_token_response(resp, "access_token")
     
     
    -def get_wts_endpoint(namespace=os.getenv("NAMESPACE", "default")):
    -    return "http://workspace-token-service.{}.svc.cluster.local".format(namespace)
    +def get_wts_endpoint(namespace=os.getenv("NAMESPACE", "default")):
    +    return "http://workspace-token-service.{}.svc.cluster.local".format(namespace)
     
     
    -def get_wts_idps(namespace=os.getenv("NAMESPACE", "default"), external_wts_host=None):
    +def get_wts_idps(namespace=os.getenv("NAMESPACE", "default"), external_wts_host=None):
         wts_url = None
         if external_wts_host == None:
             wts_url = get_wts_endpoint(namespace)
         else:
             wts_url = external_wts_host
    -    url = wts_url.rstrip("/") + "/external_oidc/"
    +    url = wts_url.rstrip("/") + "/external_oidc/"
         resp = requests.get(url)
         raise_for_status_and_print_error(resp)
         return resp.json()
     
     
     def get_token_cache_file_name(key):
    -    """Compute the path to the access-token cache file"""
    -    cache_folder = "{}/.cache/gen3/".format(os.path.expanduser("~"))
    +    """Compute the path to the access-token cache file"""
    +    cache_folder = "{}/.cache/gen3/".format(os.path.expanduser("~"))
         os.makedirs(cache_folder, exist_ok=True)
     
    -    cache_prefix = cache_folder + "token_cache_"
    +    cache_prefix = cache_folder + "token_cache_"
         s = hashlib.sha256()
    -    s.update(key.encode("utf-8"))
    +    s.update(key.encode("utf-8"))
         return cache_prefix + s.hexdigest()
     
     
     
    [docs] class Gen3Auth(AuthBase): - """Gen3 auth helper class for use with requests auth. + """Gen3 auth helper class for use with requests auth. Implements requests.auth.AuthBase in order to support JWT authentication. Generates access tokens from the provided refresh token file or string. @@ -164,17 +164,17 @@

    Source code for gen3.auth

     
         Args:
             refresh_file (str, opt): The file containing the downloaded JSON web token. Optional if working in a Gen3 Workspace.
    -                Defaults to (env["GEN3_API_KEY"] || "credentials") if refresh_token and idp not set.
    +                Defaults to (env["GEN3_API_KEY"] || "credentials") if refresh_token and idp not set.
                     Includes ~/.gen3/ in search path if value does not include /.
    -                Interprets "idp://wts/<idp>" as an idp.
    -                Interprets "accesstoken:///<token>" as an access token
    +                Interprets "idp://wts/<idp>" as an idp.
    +                Interprets "accesstoken:///<token>" as an access token
             refresh_token (str, opt): The JSON web token. Optional if working in a Gen3 Workspace.
             idp (str, opt): If working in a Gen3 Workspace, the IDP to use can be specified -
    -                "local" indicates the local environment fence idp
    +                "local" indicates the local environment fence idp
             client_credentials (tuple, opt): The (client_id, client_secret) credentials for an OIDC client
    -                that has the 'client_credentials' grant, allowing it to obtain access tokens.
    +                that has the 'client_credentials' grant, allowing it to obtain access tokens.
             client_scopes (str, opt): Space-separated list of scopes requested for access tokens obtained from client
    -                credentials. Default: "user data openid"
    +                credentials. Default: "user data openid"
             access_token (str, opt): provide an access token to override the use of any
                     API key/refresh token. This is intended for cases where you may want to
                     pass a token that was issued to a particular OIDC client (rather than acting on
    @@ -189,30 +189,30 @@ 

    Source code for gen3.auth

     
             or use ~/.gen3/crdc.json:
     
    -        >>> auth = Gen3Auth(refresh_file="crdc")
    +        >>> auth = Gen3Auth(refresh_file="crdc")
     
             or use some arbitrary file:
     
    -        >>> auth = Gen3Auth(refresh_file="./key.json")
    +        >>> auth = Gen3Auth(refresh_file="./key.json")
     
             or set the GEN3_API_KEY environment variable rather
             than pass the refresh_file argument to the Gen3Auth
             constructor.
     
    -        If working with an OIDC client that has the 'client_credentials' grant, allowing it to obtain
    +        If working with an OIDC client that has the 'client_credentials' grant, allowing it to obtain
             access tokens, provide the client ID and secret:
     
             Note: client secrets should never be hardcoded!
     
             >>> auth = Gen3Auth(
    -            endpoint="https://datacommons.example",
    -            client_credentials=("client ID", os.environ["GEN3_OIDC_CLIENT_CREDS_SECRET"])
    +            endpoint="https://datacommons.example",
    +            client_credentials=("client ID", os.environ["GEN3_OIDC_CLIENT_CREDS_SECRET"])
             )
     
             If working in a Gen3 Workspace, initialize as follows:
     
             >>> auth = Gen3Auth()
    -    """
    +    """
     
         def __init__(
             self,
    @@ -224,40 +224,40 @@ 

    Source code for gen3.auth

             client_scopes=None,
             access_token=None,
         ):
    -        logging.debug("Initializing auth..")
    +        logging.debug("Initializing auth..")
             self.endpoint = remove_trailing_whitespace_and_slashes_in_url(endpoint)
    -        # note - `_refresh_token` is not actually a JWT refresh token - it's a
    -        #  gen3 api key with a token as the "api_key" property
    +        # note - `_refresh_token` is not actually a JWT refresh token - it's a
    +        #  gen3 api key with a token as the "api_key" property
             self._refresh_token = refresh_token
             self._access_token = access_token
             self._access_token_info = None
    -        self._wts_idp = idp or "local"
    -        self._wts_namespace = os.environ.get("NAMESPACE", "default")
    +        self._wts_idp = idp or "local"
    +        self._wts_namespace = os.environ.get("NAMESPACE", "default")
             self._use_wts = False
             self._external_wts_host = None
             self._refresh_file = refresh_file
             self._client_credentials = client_credentials
             if self._client_credentials:
    -            self._client_scopes = client_scopes or "user data openid"
    +            self._client_scopes = client_scopes or "user data openid"
             elif client_scopes:
                 raise ValueError(
    -                "'client_scopes' cannot be specified without 'client_credentials'"
    +                "'client_scopes' cannot be specified without 'client_credentials'"
                 )
     
             if refresh_file and refresh_token:
                 raise ValueError(
    -                "Only one of 'refresh_file' and 'refresh_token' can be specified."
    +                "Only one of 'refresh_file' and 'refresh_token' can be specified."
                 )
     
             if endpoint and idp:
    -            raise ValueError("Only one of 'endpoint' and 'idp' can be specified.")
    +            raise ValueError("Only one of 'endpoint' and 'idp' can be specified.")
     
             if not refresh_file and not refresh_token and not idp:
    -            refresh_file = os.getenv("GEN3_API_KEY", "credentials")
    +            refresh_file = os.getenv("GEN3_API_KEY", "credentials")
     
             if refresh_file and not idp:
    -            idp_prefix = "idp://wts/"
    -            access_token_prefix = "accesstoken:///"
    +            idp_prefix = "idp://wts/"
    +            access_token_prefix = "accesstoken:///"
                 if refresh_file[0 : len(idp_prefix)] == idp_prefix:
                     idp = refresh_file[len(idp_prefix) :]
                     refresh_file = None
    @@ -267,22 +267,22 @@ 

    Source code for gen3.auth

                     refresh_file = None
                 elif (
                     not os.path.isfile(refresh_file)
    -                and "/" not in refresh_file
    -                and "\\" not in refresh_file
    +                and "/" not in refresh_file
    +                and "\\" not in refresh_file
                 ):
    -                refresh_file = "{}/.gen3/{}".format(
    -                    os.path.expanduser("~"), refresh_file
    +                refresh_file = "{}/.gen3/{}".format(
    +                    os.path.expanduser("~"), refresh_file
                     )
    -                if not os.path.isfile(refresh_file) and refresh_file[-5:] != ".json":
    -                    refresh_file += ".json"
    +                if not os.path.isfile(refresh_file) and refresh_file[-5:] != ".json":
    +                    refresh_file += ".json"
                     if not os.path.isfile(refresh_file):
    -                    logging.warning("Unable to find refresh_file")
    +                    logging.warning("Unable to find refresh_file")
                         refresh_file = None
     
             if self._client_credentials:
                 if not endpoint:
                     raise ValueError(
    -                    "'endpoint' must be specified when '_client_credentials' is specified"
    +                    "'endpoint' must be specified when '_client_credentials' is specified"
                     )
                 self._access_token = get_access_token_with_client_credentials(
                     endpoint, self._client_credentials, self._client_scopes
    @@ -292,7 +292,7 @@ 

    Source code for gen3.auth

                 # at this point - refresh_file either exists or is None
                 if not refresh_file and not refresh_token:
                     # check if this is a Gen3 workspace environment
    -                # most production environments are in the "default" namespace
    +                # most production environments are in the "default" namespace
                     # attempt to get a token from the workspace-token-service
                     self._use_wts = True
                     # hate calling a method from the constructor, but avoids copying code
    @@ -304,34 +304,34 @@ 

    Source code for gen3.auth

                         self._refresh_token = json.loads(file_data)
                     except Exception as e:
                         raise ValueError(
    -                        "Couldn't load your refresh token file: {}\n{}".format(
    +                        "Couldn't load your refresh token file: {}\n{}".format(
                                 refresh_file, str(e)
                             )
                         )
     
    -                assert "api_key" in self._refresh_token
    +                assert "api_key" in self._refresh_token
                     # if both endpoint and refresh file are provided, compare endpoint with iss in refresh file
                     # May need to use network wts endpoint
                     if idp or (
                         endpoint
                         and (
    -                        not endpoint.rstrip("/")
    -                        == endpoint_from_token(self._refresh_token["api_key"])
    +                        not endpoint.rstrip("/")
    +                        == endpoint_from_token(self._refresh_token["api_key"])
                         )
                     ):
                         try:
                             logging.debug(
    -                            "Switch to using WTS and set external WTS host url.."
    +                            "Switch to using WTS and set external WTS host url.."
                             )
                             self._use_wts = True
                             self._external_wts_host = (
    -                            endpoint_from_token(self._refresh_token["api_key"])
    -                            + "/wts/"
    +                            endpoint_from_token(self._refresh_token["api_key"])
    +                            + "/wts/"
                             )
                             self.get_access_token()
                         except Gen3AuthError as g:
                             logging.warning(
    -                            "Could not obtain access token from WTS service."
    +                            "Could not obtain access token from WTS service."
                             )
                             raise g
     
    @@ -339,20 +339,20 @@ 

    Source code for gen3.auth

                 if self._access_token:
                     self.endpoint = endpoint_from_token(self._access_token)
                 else:
    -                self.endpoint = endpoint_from_token(self._refresh_token["api_key"])
    +                self.endpoint = endpoint_from_token(self._refresh_token["api_key"])
     
         @property
         def _token_info(self):
    -        """
    +        """
             Wrapper to fix intermittent errors when the token is being refreshed
             and `_access_token_info` == None
    -        """
    +        """
             if not self._access_token_info:
                 self.refresh_access_token()
             return self._access_token_info
     
         def __call__(self, request):
    -        """Adds authorization header to the request
    +        """Adds authorization header to the request
     
             This gets called by the python.requests package on outbound requests
             so that authentication can be added.
    @@ -360,13 +360,13 @@ 

    Source code for gen3.auth

             Args:
                 request (object): The incoming request object
     
    -        """
    -        request.headers["Authorization"] = self._get_auth_value()
    -        request.register_hook("response", self._handle_401)
    +        """
    +        request.headers["Authorization"] = self._get_auth_value()
    +        request.register_hook("response", self._handle_401)
             return request
     
         def _handle_401(self, response, **kwargs):
    -        """Handles failed requests when authorization failed.
    +        """Handles failed requests when authorization failed.
     
             This gets called after a failed request when an HTTP 401 error
             occurs. This then tries to refresh the access token in the event
    @@ -375,7 +375,7 @@ 

    Source code for gen3.auth

             Args:
                 request (object): The failed request object
     
    -        """
    +        """
             if not response.status_code == 401 and not response.status_code == 403:
                 return response
     
    @@ -386,7 +386,7 @@ 

    Source code for gen3.auth

             # copy the request to resend
             newreq = response.request.copy()
     
    -        newreq.headers["Authorization"] = self._get_auth_value()
    +        newreq.headers["Authorization"] = self._get_auth_value()
     
             _response = response.connection.send(newreq, **kwargs)
             _response.history.append(response)
    @@ -397,7 +397,7 @@ 

    Source code for gen3.auth

     
    [docs] def refresh_access_token(self, endpoint=None): - """Get a new access token""" + """Get a new access token""" if self._use_wts: self._access_token = self.get_access_token_from_wts(endpoint) elif self._client_credentials: @@ -408,8 +408,8 @@

    Source code for gen3.auth

                 self._access_token = get_access_token_with_key(self._refresh_token)
             else:
                 logging.warning(
    -                f"Unable to refresh access token. "
    -                f"Authorized API calls will stop working when this token expires."
    +                f"Unable to refresh access token. "
    +                f"Authorized API calls will stop working when this token expires."
                 )
     
             self._access_token_info = decode_token(self._access_token)
    @@ -418,14 +418,14 @@ 

    Source code for gen3.auth

             if self._use_wts:
                 cache_file = get_token_cache_file_name(self._wts_idp)
             elif self._refresh_file:
    -            cache_file = get_token_cache_file_name(self._refresh_token["api_key"])
    +            cache_file = get_token_cache_file_name(self._refresh_token["api_key"])
     
             if cache_file:
                 try:
                     self._write_to_file(cache_file, self._access_token)
                 except Exception as e:
                     logging.warning(
    -                    f"Unable to write access token to cache file. Exceeded number of retries. Details: {e}"
    +                    f"Unable to write access token to cache file. Exceeded number of retries. Details: {e}"
                     )
     
             return self._access_token
    @@ -438,37 +438,37 @@

    Source code for gen3.auth

             # write a temp file, then rename - to avoid
             # simultaneous writes to same file race condition
             temp = cache_file + (
    -            ".tmp_eraseme_%d_%d" % (random.randrange(100000), time.time())
    +            ".tmp_eraseme_%d_%d" % (random.randrange(100000), time.time())
             )
             try:
    -            with open(temp, "w") as f:
    +            with open(temp, "w") as f:
                     f.write(content)
                 os.rename(temp, cache_file)
                 return True
             except Exception as e:
    -            logging.warning("failed to write token cache file: " + cache_file)
    +            logging.warning("failed to write token cache file: " + cache_file)
                 logging.warning(str(e))
                 raise e
     
     
    [docs] def get_access_token(self): - """Get the access token - auto refresh if within 5 minutes of expiration""" + """Get the access token - auto refresh if within 5 minutes of expiration""" if not self._access_token: if self._use_wts == True: cache_file = get_token_cache_file_name(self._wts_idp) else: if self._refresh_token: cache_file = get_token_cache_file_name( - self._refresh_token["api_key"] + self._refresh_token["api_key"] ) if cache_file and os.path.isfile(cache_file): - try: # don't freak out on invalid cache + try: # don't freak out on invalid cache with open(cache_file) as f: self._access_token = f.read() self._access_token_info = decode_token(self._access_token) except Exception as e: - logging.warning("ignoring invalid token cache: " + cache_file) + logging.warning("ignoring invalid token cache: " + cache_file) self._access_token = None self._access_token_info = None logging.warning(str(e)) @@ -476,11 +476,11 @@

    Source code for gen3.auth

             need_new_token = (
                 not self._access_token
                 or not self._access_token_info
    -            or time.time() + 300 > self._access_token_info["exp"]
    +            or time.time() + 300 > self._access_token_info["exp"]
             )
             if need_new_token:
                 return self.refresh_access_token(
    -                self.endpoint if hasattr(self, "endpoint") else None
    +                self.endpoint if hasattr(self, "endpoint") else None
                 )
             # use cache
             return self._access_token
    @@ -489,108 +489,108 @@

    Source code for gen3.auth

     
    [docs] def get_api_key(self): - """ + """ Returns the actual API key loaded, or None if the loaded credentials do not contain one (for example a raw access token). - """ + """ if not self._refresh_token: return None - return self._refresh_token.get("api_key")
    + return self._refresh_token.get("api_key")
    def _get_auth_value(self): - """Returns the Authorization header value for the request + """Returns the Authorization header value for the request This gets called when added the Authorization header to the request. This fetches the access token from the refresh token if the access token is missing. - """ - return "bearer " + self.get_access_token() + """ + return "bearer " + self.get_access_token()
    [docs] def curl(self, path, request=None, data=None): - """ + """ Curl the given endpoint - ex: gen3 curl /user/user. Return requests.Response Args: path (str): path under the commons to curl (/user/user, /index/index, /authz/mapping, ...) request (str in GET|POST|PUT|DELETE): default to GET if data is not set, else default to POST - data (str): json string or "@filename" of a json file - """ + data (str): json string or "@filename" of a json file + """ if not request: - request = "GET" + request = "GET" if data: - request = "POST" + request = "POST" json_data = data output = None - if data and data[0] == "@": + if data and data[0] == "@": with open(data[1:]) as f: json_data = f.read() - if request == "GET": - output = requests.get(self.endpoint + "/" + path, auth=self) - elif request == "POST": + if request == "GET": + output = requests.get(self.endpoint + "/" + path, auth=self) + elif request == "POST": output = requests.post( - self.endpoint + "/" + path, json=json_data, auth=self + self.endpoint + "/" + path, json=json_data, auth=self ) - elif request == "PUT": - output = requests.put(self.endpoint + "/" + path, json=json_data, auth=self) - elif request == "DELETE": - output = requests.delete(self.endpoint + "/" + path, auth=self) + elif request == "PUT": + output = requests.put(self.endpoint + "/" + path, json=json_data, auth=self) + elif request == "DELETE": + output = requests.delete(self.endpoint + "/" + path, auth=self) else: - raise Exception("Invalid request type: " + request) + raise Exception("Invalid request type: " + request) return output
    [docs] def get_access_token_from_wts(self, endpoint=None): - """ + """ Try to fetch an access token for the given idp from the wts - in the given namespace. If idp is not set, then default to "local" - """ + in the given namespace. If idp is not set, then default to "local" + """ # attempt to get a token from the workspace-token-service - logging.debug("getting access token from wts..") - auth_url = get_wts_endpoint(self._wts_namespace) + "/token/" + logging.debug("getting access token from wts..") + auth_url = get_wts_endpoint(self._wts_namespace) + "/token/" - # If non "local" idp value exists, append to auth url + # If non "local" idp value exists, append to auth url # If user specified endpoint value, then first attempt to determine idp value. - if self.endpoint or (self._wts_idp and self._wts_idp != "local"): + if self.endpoint or (self._wts_idp and self._wts_idp != "local"): # If user supplied endpoint value and not idp, figure out the idp value if self.endpoint: logging.debug( - "First try to use the local WTS to figure out idp name for the supplied endpoint.." + "First try to use the local WTS to figure out idp name for the supplied endpoint.." ) try: provider_List = get_wts_idps(self._wts_namespace) matchProviders = list( filter( - lambda provider: provider["base_url"] == endpoint, - provider_List["providers"], + lambda provider: provider["base_url"] == endpoint, + provider_List["providers"], ) ) if len(matchProviders) == 1: - logging.debug("Found matching idp from local WTS.") - self._wts_idp = matchProviders[0]["idp"] + logging.debug("Found matching idp from local WTS.") + self._wts_idp = matchProviders[0]["idp"] elif len(matchProviders) > 1: raise ValueError( - "Multiple idps matched with endpoint value provided." + "Multiple idps matched with endpoint value provided." ) else: - logging.debug("Could not find matching idp from local WTS.") + logging.debug("Could not find matching idp from local WTS.") except Exception as e: logging.debug( - "Exception occured when making network call to local WTS." + "Exception occured when making network call to local WTS." ) if not self._external_wts_host: raise e else: - logging.debug("Since external WTS host exists, continuing on..") + logging.debug("Since external WTS host exists, continuing on..") pass - if self._wts_idp and self._wts_idp != "local": - auth_url += "?idp={}".format(self._wts_idp) + if self._wts_idp and self._wts_idp != "local": + auth_url += "?idp={}".format(self._wts_idp) # If endpoint value exists, only get WTS token if idp value has been successfully determined # Otherwise skip to querying external WTS @@ -598,97 +598,97 @@

    Source code for gen3.auth

             if (
                 not self._external_wts_host
                 or not self.endpoint
    -            or (self.endpoint and self._wts_idp != "local")
    +            or (self.endpoint and self._wts_idp != "local")
             ):
                 try:
    -                logging.debug("Try to get access token from local WTS..")
    -                logging.debug(f"{auth_url=}")
    +                logging.debug("Try to get access token from local WTS..")
    +                logging.debug(f"{auth_url=}")
                     resp = requests.get(auth_url)
                     if (resp and resp.status_code == 200) or (not self._external_wts_host):
    -                    return _handle_access_token_response(resp, "token")
    +                    return _handle_access_token_response(resp, "token")
                 except Exception as e:
                     if not self._external_wts_host:
                         raise e
                     else:
                         # Try to obtain token from external wts
    -                    logging.debug("Could get obtain token from Local WTS.")
    +                    logging.debug("Could get obtain token from Local WTS.")
                         pass
     
             # local workspace wts call failed, try using a network call
             # First get access token with WTS host
    -        logging.debug("Trying to get access token from external WTS Host..")
    +        logging.debug("Trying to get access token from external WTS Host..")
             wts_token = get_access_token_with_key(self._refresh_token)
    -        auth_url = self._external_wts_host + "token/"
    +        auth_url = self._external_wts_host + "token/"
     
             provider_List = get_wts_idps(self._wts_namespace, self._external_wts_host)
     
             # if user already supplied idp, use that
    -        if self._wts_idp and self._wts_idp != "local":
    +        if self._wts_idp and self._wts_idp != "local":
                 matchProviders = list(
                     filter(
    -                    lambda provider: provider["idp"] == self._wts_idp,
    -                    provider_List["providers"],
    +                    lambda provider: provider["idp"] == self._wts_idp,
    +                    provider_List["providers"],
                     )
                 )
             elif endpoint:
                 matchProviders = list(
                     filter(
    -                    lambda provider: provider["base_url"] == endpoint,
    -                    provider_List["providers"],
    +                    lambda provider: provider["base_url"] == endpoint,
    +                    provider_List["providers"],
                     )
                 )
             else:
                 raise Exception(
    -                "Unable to generate matching identity providers (no IdP or endpoint provided)"
    +                "Unable to generate matching identity providers (no IdP or endpoint provided)"
                 )
     
             if len(matchProviders) == 1:
    -            self._wts_idp = matchProviders[0]["idp"]
    -            logging.debug("Succesfully determined idp value: {}".format(self._wts_idp))
    +            self._wts_idp = matchProviders[0]["idp"]
    +            logging.debug("Succesfully determined idp value: {}".format(self._wts_idp))
             else:
    -            idp_list = "\n "
    +            idp_list = "\n "
     
                 if len(matchProviders) > 1:
                     for idp in matchProviders:
                         idp_list = (
                             idp_list
    -                        + "idp name: "
    -                        + idp["idp"]
    -                        + " url: "
    -                        + idp["base_url"]
    -                        + "\n "
    +                        + "idp name: "
    +                        + idp["idp"]
    +                        + " url: "
    +                        + idp["base_url"]
    +                        + "\n "
                         )
                     raise ValueError(
    -                    "Multiple idps matched with endpoint value provided."
    +                    "Multiple idps matched with endpoint value provided."
                         + idp_list
    -                    + "Query /wts/external_oidc/ for more information."
    +                    + "Query /wts/external_oidc/ for more information."
                     )
                 else:
    -                for idp in provider_List["providers"]:
    +                for idp in provider_List["providers"]:
                         idp_list = (
                             idp_list
    -                        + "idp name: "
    -                        + idp["idp"]
    -                        + "  Endpoint url: "
    -                        + idp["base_url"]
    -                        + "\n "
    +                        + "idp name: "
    +                        + idp["idp"]
    +                        + "  Endpoint url: "
    +                        + idp["base_url"]
    +                        + "\n "
                         )
                     raise ValueError(
    -                    "No idp matched with the endpoint or idp value provided.\n"
    -                    + "Please make sure your endpoint or idp value matches exactly with the output below.\n"
    -                    + "i.e. check trailing '/' character for the endpoint url\n"
    -                    + "Available Idps:"
    +                    "No idp matched with the endpoint or idp value provided.\n"
    +                    + "Please make sure your endpoint or idp value matches exactly with the output below.\n"
    +                    + "i.e. check trailing '/' character for the endpoint url\n"
    +                    + "Available Idps:"
                         + idp_list
    -                    + "Query /wts/external_oidc/ for more information."
    +                    + "Query /wts/external_oidc/ for more information."
                     )
    -        logging.debug("Finally getting access token..")
    -        auth_url += "?idp={}".format(self._wts_idp)
    -        header = {"Authorization": "Bearer " + wts_token}
    +        logging.debug("Finally getting access token..")
    +        auth_url += "?idp={}".format(self._wts_idp)
    +        header = {"Authorization": "Bearer " + wts_token}
             resp = requests.get(auth_url, headers=header)
    -        err_msg = "Please make sure the target commons is connected on your profile page and that connection has not expired."
    +        err_msg = "Please make sure the target commons is connected on your profile page and that connection has not expired."
             if resp.status_code != 200:
                 logging.warning(err_msg)
    -        return _handle_access_token_response(resp, "token")
    + return _handle_access_token_response(resp, "token")
    diff --git a/docs/_build/html/_modules/gen3/file.html b/docs/_build/html/_modules/gen3/file.html index 53916adc8..1cd1cbfa6 100644 --- a/docs/_build/html/_modules/gen3/file.html +++ b/docs/_build/html/_modules/gen3/file.html @@ -59,7 +59,7 @@

    Source code for gen3.file

     
    [docs] class Gen3File: - """For interacting with Gen3 file management features. + """For interacting with Gen3 file management features. A class for interacting with the Gen3 file download services. Supports getting presigned urls right now. @@ -71,10 +71,10 @@

    Source code for gen3.file

             This generates the Gen3File class pointed at the sandbox commons while
             using the credentials.json downloaded from the commons profile page.
     
    -        >>> auth = Gen3Auth(refresh_file="credentials.json")
    +        >>> auth = Gen3Auth(refresh_file="credentials.json")
             ... file = Gen3File(auth)
     
    -    """
    +    """
     
         def __init__(self, endpoint=None, auth_provider=None):
             # auth_provider legacy interface required endpoint as 1st arg
    @@ -85,7 +85,7 @@ 

    Source code for gen3.file

     
    [docs] def get_presigned_url(self, guid, protocol=None): - """Generates a presigned URL for a file. + """Generates a presigned URL for a file. Retrieves a presigned url for a file giving access to a file for a limited time. @@ -97,10 +97,10 @@

    Source code for gen3.file

     
                 >>> Gen3File.get_presigned_url(query)
     
    -        """
    -        api_url = "{}/user/data/download/{}".format(self._endpoint, guid)
    +        """
    +        api_url = "{}/user/data/download/{}".format(self._endpoint, guid)
             if protocol:
    -            api_url += "?protocol={}".format(protocol)
    +            api_url += "?protocol={}".format(protocol)
             resp = requests.get(api_url, auth=self._auth_provider)
             raise_for_status_and_print_error(resp)
     
    @@ -113,7 +113,7 @@ 

    Source code for gen3.file

     
    [docs] def delete_file(self, guid): - """ + """ This method is DEPRECATED. Use delete_file_locations() instead. Delete all locations of a stored data file and remove its record from indexd @@ -121,9 +121,9 @@

    Source code for gen3.file

                 guid (str): provide a UUID for file id to delete
             Returns:
                 text: requests.delete text result
    -        """
    -        print("This method is DEPRECATED. Use delete_file_locations() instead.")
    -        api_url = "{}/user/data/{}".format(self._endpoint, guid)
    +        """
    +        print("This method is DEPRECATED. Use delete_file_locations() instead.")
    +        api_url = "{}/user/data/{}".format(self._endpoint, guid)
             output = requests.delete(api_url, auth=self._auth_provider).text
     
             return output
    @@ -132,15 +132,15 @@

    Source code for gen3.file

     
    [docs] def delete_file_locations(self, guid): - """ + """ Delete all locations of a stored data file and remove its record from indexd Args: guid (str): provide a UUID for file id to delete Returns: requests.Response : requests.delete result - """ - api_url = "{}/user/data/{}".format(self._endpoint, guid) + """ + api_url = "{}/user/data/{}".format(self._endpoint, guid) output = requests.delete(api_url, auth=self._auth_provider) return output
    @@ -151,37 +151,37 @@

    Source code for gen3.file

         def upload_file(
             self, file_name, authz=None, protocol=None, expires_in=None, bucket=None
         ):
    -        """
    +        """
             Get a presigned url for a file to upload
     
             Args:
                 file_name (str): file_name to use for upload
                 authz (list): authorization scope for the file as list of paths, optional.
    -            protocol (str): Storage protocol to use for upload: "s3", "az".
    -                If this isn't set, the default will be "s3"
    +            protocol (str): Storage protocol to use for upload: "s3", "az".
    +                If this isn't set, the default will be "s3"
                 expires_in (int): Amount in seconds that the signed url will expire from datetime.utcnow().
                     Be sure to use a positive integer.
                     This value will also be treated as <= MAX_PRESIGNED_URL_TTL in the fence configuration.
    -            bucket (str): Bucket to upload to. The bucket must be configured in the Fence instance's
    +            bucket (str): Bucket to upload to. The bucket must be configured in the Fence instance's
                     `ALLOWED_DATA_UPLOAD_BUCKETS` setting. If not specified, Fence defaults to the
                     `DATA_UPLOAD_BUCKET` setting.
             Returns:
                 Document: json representation for the file upload
    -        """
    -        api_url = f"{self._endpoint}/user/data/upload"
    +        """
    +        api_url = f"{self._endpoint}/user/data/upload"
             body = {}
             if protocol:
    -            body["protocol"] = protocol
    +            body["protocol"] = protocol
             if authz:
    -            body["authz"] = authz
    +            body["authz"] = authz
             if expires_in:
    -            body["expires_in"] = expires_in
    +            body["expires_in"] = expires_in
             if file_name:
    -            body["file_name"] = file_name
    +            body["file_name"] = file_name
             if bucket:
    -            body["bucket"] = bucket
    +            body["bucket"] = bucket
     
    -        headers = {"Content-Type": "application/json"}
    +        headers = {"Content-Type": "application/json"}
             resp = requests.post(
                 api_url, auth=self._auth_provider, json=body, headers=headers
             )
    @@ -195,13 +195,13 @@ 

    Source code for gen3.file

     
     
         def _ensure_dirpath_exists(path: Path) -> Path:
    -        """Utility to create a directory if missing.
    +        """Utility to create a directory if missing.
             Returns the path so that the call can be inlined in another call
             Args:
                 path (Path): path to create
             Returns
                 path of created directory
    -        """
    +        """
             assert path
             out_path: Path = path
     
    @@ -213,58 +213,58 @@ 

    Source code for gen3.file

     
    [docs] def download_single(self, object_id, path): - """ + """ Download a single file using its GUID. Args: - object_id (str): The file's unique ID + object_id (str): The file's unique ID path (str): Path to store the downloaded file at - """ + """ try: url = self.get_presigned_url(object_id) except Exception as e: - logging.critical(f"Unable to get a presigned URL for download: {e}") + logging.critical(f"Unable to get a presigned URL for download: {e}") return False - response = requests.get(url["url"], stream=True) + response = requests.get(url["url"], stream=True) if response.status_code != 200: - logging.error(f"Response code: {response.status_code}") + logging.error(f"Response code: {response.status_code}") if response.status_code >= 500: for _ in range(MAX_RETRIES): - logging.info("Retrying now...") + logging.info("Retrying now...") # NOTE could be updated with exponential backoff time.sleep(1) - response = requests.get(url["url"], stream=True) + response = requests.get(url["url"], stream=True) if response.status == 200: break if response.status != 200: - logging.critical("Response status not 200, try again later") + logging.critical("Response status not 200, try again later") return False else: return False response.raise_for_status() - total_size_in_bytes = int(response.headers.get("content-length")) + total_size_in_bytes = int(response.headers.get("content-length")) total_downloaded = 0 index = Gen3Index(self._auth_provider) record = index.get_record(object_id) - filename = record["file_name"] + filename = record["file_name"] out_path = Gen3File._ensure_dirpath_exists(Path(path)) - with open(os.path.join(out_path, filename), "wb") as f: + with open(os.path.join(out_path, filename), "wb") as f: for data in response.iter_content(4096): total_downloaded += len(data) f.write(data) if total_size_in_bytes == total_downloaded: - logging.info(f"File {filename} downloaded successfully") + logging.info(f"File {filename} downloaded successfully") else: - logging.error(f"File {filename} not downloaded successfully") + logging.error(f"File {filename} not downloaded successfully") return False return True
    @@ -275,32 +275,32 @@

    Source code for gen3.file

         def upload_file_to_guid(
             self, guid, file_name, protocol=None, expires_in=None, bucket=None
         ):
    -        """
    +        """
             Get a presigned url for a file to upload to the specified existing GUID
     
             Args:
                 file_name (str): file_name to use for upload
    -            protocol (str): Storage protocol to use for upload: "s3", "az".
    -                If this isn't set, the default will be "s3"
    +            protocol (str): Storage protocol to use for upload: "s3", "az".
    +                If this isn't set, the default will be "s3"
                 expires_in (int): Amount in seconds that the signed url will expire from datetime.utcnow().
                     Be sure to use a positive integer.
                     This value will also be treated as <= MAX_PRESIGNED_URL_TTL in the fence configuration.
    -            bucket (str): Bucket to upload to. The bucket must be configured in the Fence instance's
    +            bucket (str): Bucket to upload to. The bucket must be configured in the Fence instance's
                     `ALLOWED_DATA_UPLOAD_BUCKETS` setting. If not specified, Fence defaults to the
                     `DATA_UPLOAD_BUCKET` setting.
             Returns:
                 Document: json representation for the file upload
    -        """
    -        url = f"{self._endpoint}/user/data/upload/{guid}"
    +        """
    +        url = f"{self._endpoint}/user/data/upload/{guid}"
             params = {}
             if protocol:
    -            params["protocol"] = protocol
    +            params["protocol"] = protocol
             if expires_in:
    -            params["expires_in"] = expires_in
    +            params["expires_in"] = expires_in
             if file_name:
    -            params["file_name"] = file_name
    +            params["file_name"] = file_name
             if bucket:
    -            params["bucket"] = bucket
    +            params["bucket"] = bucket
     
             url_parts = list(urlparse(url))
             query = dict(parse_qsl(url_parts[4]))
    diff --git a/docs/_build/html/_modules/gen3/index.html b/docs/_build/html/_modules/gen3/index.html
    index b393f29c4..694009f6e 100644
    --- a/docs/_build/html/_modules/gen3/index.html
    +++ b/docs/_build/html/_modules/gen3/index.html
    @@ -50,7 +50,7 @@ 

    Source code for gen3.index

     
    [docs] class Gen3Index: - """ + """ A class for interacting with the Gen3 Index services. @@ -62,26 +62,26 @@

    Source code for gen3.index

             This generates the Gen3Index class pointed at the sandbox commons while
             using the credentials.json downloaded from the commons profile page.
     
    -        >>> auth = Gen3Auth(refresh_file="credentials.json")
    +        >>> auth = Gen3Auth(refresh_file="credentials.json")
             ... index = Gen3Index(auth)
     
    -    """
    +    """
     
    -    def __init__(self, endpoint=None, auth_provider=None, service_location="index"):
    +    def __init__(self, endpoint=None, auth_provider=None, service_location="index"):
             # legacy interface required endpoint as 1st arg
             if endpoint and isinstance(endpoint, Gen3Auth):
                 auth_provider = endpoint
                 endpoint = None
             if auth_provider and isinstance(auth_provider, Gen3Auth):
                 endpoint = auth_provider.endpoint
    -        endpoint = endpoint.strip("/")
    +        endpoint = endpoint.strip("/")
             # if running locally, indexd is deployed by itself without a location relative
             # to the commons
    -        if "http://localhost" in endpoint:
    -            service_location = ""
    +        if "http://localhost" in endpoint:
    +            service_location = ""
     
             if not endpoint.endswith(service_location):
    -            endpoint += "/" + service_location
    +            endpoint += "/" + service_location
     
             self.endpoint = endpoint
             self.client = client.IndexClient(endpoint, auth=auth_provider)
    @@ -90,29 +90,29 @@ 

    Source code for gen3.index

     
    [docs] def is_healthy(self): - """ + """ Return if indexd is healthy or not - """ + """ try: - response = self.client._get("_status") + response = self.client._get("_status") response.raise_for_status() except Exception: return False - return response.text == "Healthy"
    + return response.text == "Healthy"
    [docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) def get_version(self): - """ + """ Return the version of indexd - """ - response = self.client._get("_version") + """ + response = self.client._get("_version") raise_for_status_and_print_error(response) return response.json()
    @@ -121,12 +121,12 @@

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_stats(self):
    -        """
    +        """
     
             Return basic info about the records in indexd
     
    -        """
    -        response = self.client._get("_stats")
    +        """
    +        response = self.client._get("_stats")
             raise_for_status_and_print_error(response)
             return response.json()
    @@ -135,31 +135,31 @@

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_all_records(self, limit=None, paginate=False):
    -        """
    +        """
     
             Get a list of all records
     
    -        """
    +        """
             all_records = []
    -        url = "index/"
    +        url = "index/"
     
             if limit:
    -            url += f"?limit={limit}"
    +            url += f"?limit={limit}"
     
             response = self.client._get(url)
             raise_for_status_and_print_error(response)
     
    -        records = response.json().get("records")
    +        records = response.json().get("records")
             all_records.extend(records)
     
             if paginate and records:
                 previous_did = None
    -            start_did = records[-1].get("did")
    +            start_did = records[-1].get("did")
     
                 while start_did != previous_did:
                     previous_did = start_did
     
    -                params = {"start": f"{start_did}"}
    +                params = {"start": f"{start_did}"}
                     url_parts = list(urllib.parse.urlparse(url))
                     query = dict(urllib.parse.parse_qsl(url_parts[4]))
                     query.update(params)
    @@ -170,11 +170,11 @@ 

    Source code for gen3.index

                     response = self.client._get(url)
                     raise_for_status_and_print_error(response)
     
    -                records = response.json().get("records")
    +                records = response.json().get("records")
                     all_records.extend(records)
     
                     if records:
    -                    start_did = response.json().get("records")[-1].get("did")
    +                    start_did = response.json().get("records")[-1].get("did")
     
             return all_records
    @@ -183,33 +183,33 @@

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_records_on_page(self, limit=None, page=None):
    -        """
    +        """
     
             Get a list of all records given the page and page size limit
     
    -        """
    +        """
             params = {}
    -        url = "index/"
    +        url = "index/"
     
             if limit is not None:
    -            params["limit"] = limit
    +            params["limit"] = limit
     
             if page is not None:
    -            params["page"] = page
    +            params["page"] = page
     
             query = urllib.parse.urlencode(params)
     
    -        response = self.client._get(url + "?" + query)
    +        response = self.client._get(url + "?" + query)
             raise_for_status_and_print_error(response)
     
    -        return response.json().get("records")
    + return response.json().get("records")
    [docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) async def async_get_record(self, guid=None, _ssl=None): - """ + """ Asynchronous function to request a record from indexd. Args: @@ -217,8 +217,8 @@

    Source code for gen3.index

     
             Returns:
                 dict: indexd record
    -        """
    -        url = f"{self.client.url}/index/{guid}"
    +        """
    +        url = f"{self.client.url}/index/{guid}"
             async with aiohttp.ClientSession() as session:
                 async with session.get(url, ssl=_ssl) as response:
                     raise_for_status_and_print_error(response)
    @@ -231,7 +231,7 @@ 

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         async def async_get_records_on_page(self, limit=None, page=None, _ssl=None):
    -        """
    +        """
             Asynchronous function to request a page from indexd.
     
             Args:
    @@ -239,33 +239,33 @@ 

    Source code for gen3.index

     
             Returns:
                 List[dict]: List of indexd records from the page
    -        """
    +        """
             all_records = []
             params = {}
     
             if limit is not None:
    -            params["limit"] = limit
    +            params["limit"] = limit
     
             if page is not None:
    -            params["page"] = page
    +            params["page"] = page
     
             query = urllib.parse.urlencode(params)
     
    -        url = f"{self.client.url}/index" + "?" + query
    +        url = f"{self.client.url}/index" + "?" + query
             async with aiohttp.ClientSession() as session:
                 async with session.get(url, ssl=_ssl) as response:
                     response = await response.json()
     
    -        return response.get("records")
    + return response.get("records")
    [docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) async def async_get_records_from_checksum( - self, checksum, checksum_type="md5", _ssl=None + self, checksum, checksum_type="md5", _ssl=None ): - """ + """ Asynchronous function to request records from indexd matching checksum. Args: @@ -274,27 +274,27 @@

    Source code for gen3.index

     
             Returns:
                 List[dict]: List of indexd records
    -        """
    +        """
             all_records = []
             params = {}
     
    -        params["hash"] = f"{checksum_type}:{checksum}"
    +        params["hash"] = f"{checksum_type}:{checksum}"
     
             query = urllib.parse.urlencode(params)
     
    -        url = f"{self.client.url}/index" + "?" + query
    +        url = f"{self.client.url}/index" + "?" + query
             async with aiohttp.ClientSession() as session:
                 async with session.get(url, ssl=_ssl) as response:
                     response = await response.json()
     
    -        return response.get("records")
    + return response.get("records")
    [docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) def get(self, guid, dist_resolution=True): - """ + """ Get the metadata associated with the given id, alias, or distributed identifier @@ -305,7 +305,7 @@

    Source code for gen3.index

                 dist_resolution: boolean
                 - *optional* Specify if we want distributed dist_resolution or not
     
    -        """
    +        """
             rec = self.client.global_get(guid, dist_resolution)
     
             if not rec:
    @@ -318,7 +318,7 @@ 

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_urls(self, size=None, hashes=None, guids=None):
    -        """
    +        """
     
             Get a list of urls that match query params
     
    @@ -330,11 +330,11 @@ 

    Source code for gen3.index

                 guids: list
                     - list of ids
     
    -        """
    +        """
             if guids:
    -            guids = ",".join(guids)
    -        p = {"size": size, "hash": hashes, "ids": guids}
    -        urls = self.client._get("urls", params=p).json()
    +            guids = ",".join(guids)
    +        p = {"size": size, "hash": hashes, "ids": guids}
    +        urls = self.client._get("urls", params=p).json()
             return [url for _, url in urls.items()]
    @@ -342,11 +342,11 @@

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_record(self, guid):
    -        """
    +        """
     
             Get the metadata associated with a given id
     
    -        """
    +        """
             rec = self.client.get(guid)
     
             if not rec:
    @@ -359,11 +359,11 @@ 

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_record_doc(self, guid):
    -        """
    +        """
     
             Get the metadata associated with a given id
     
    -        """
    +        """
             return self.client.get(guid)
    @@ -371,16 +371,16 @@

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_with_params(self, params=None):
    -        """
    +        """
     
             Return a document object corresponding to the supplied parameters, such
    -        as ``{'hashes': {'md5': '...'}, 'size': '...', 'metadata': {'file_state': '...'}}``.
    +        as ``{'hashes': {'md5': '...'}, 'size': '...', 'metadata': {'file_state': '...'}}``.
     
                 - need to include all the hashes in the request
                 - index client like signpost or indexd will need to handle the
    -              query param `'hash': 'hash_type:hash'`
    +              query param `'hash': 'hash_type:hash'`
     
    -        """
    +        """
             rec = self.client.get_with_params(params)
     
             if not rec:
    @@ -393,12 +393,12 @@ 

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         async def async_get_with_params(self, params, _ssl=None):
    -        """
    +        """
     
             Return a document object corresponding to the supplied parameter
     
             - need to include all the hashes in the request
    -        - need to handle the query param `'hash': 'hash_type:hash'`
    +        - need to handle the query param `'hash': 'hash_type:hash'`
     
             Args:
                 params (dict): params to search with
    @@ -407,9 +407,9 @@ 

    Source code for gen3.index

             Returns:
                 Document: json representation of an entry in indexd
     
    -        """
    +        """
             query_params = urllib.parse.urlencode(params)
    -        url = f"{self.client.url}/index/?{query_params}"
    +        url = f"{self.client.url}/index/?{query_params}"
             async with aiohttp.ClientSession() as session:
                 async with session.get(url, ssl=_ssl) as response:
                     await response.raise_for_status()
    @@ -422,7 +422,7 @@ 

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_latest_version(self, guid, has_version=False):
    -        """
    +        """
     
             Get the metadata of the latest index record version associated
             with the given id
    @@ -433,7 +433,7 @@ 

    Source code for gen3.index

                 has_version: boolean
                     - *optional* exclude entries without a version
     
    -        """
    +        """
             rec = self.client.get_latest_version(guid, has_version)
     
             if not rec:
    @@ -446,7 +446,7 @@ 

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_versions(self, guid):
    -        """
    +        """
     
             Get the metadata of index record version associated with the
             given id
    @@ -455,8 +455,8 @@ 

    Source code for gen3.index

                 guid: string
                     - record id
     
    -        """
    -        response = self.client._get(f"/index/{guid}/versions")
    +        """
    +        response = self.client._get(f"/index/{guid}/versions")
             raise_for_status_and_print_error(response)
             versions = response.json()
     
    @@ -485,13 +485,13 @@ 

    Source code for gen3.index

             content_created_date=None,
             content_updated_date=None,
         ):
    -        """
    +        """
     
             Create a new record and add it to the index
     
             Args:
                 hashes (dict): {hash type: hash value,}
    -                eg ``hashes={'md5': ab167e49d25b488939b1ede42752458b'}``
    +                eg ``hashes={'md5': ab167e49d25b488939b1ede42752458b'}``
                 size (int): file size metadata associated with a given uuid
                 did (str): provide a UUID for the new indexd to be made
                 urls (list): list of URLs where you can download the UUID
    @@ -508,26 +508,26 @@ 

    Source code for gen3.index

             Returns:
                 Document: json representation of an entry in indexd
     
    -        """
    +        """
             if urls is None:
                 urls = []
             json = {
    -            "urls": urls,
    -            "hashes": hashes,
    -            "size": size,
    -            "file_name": file_name,
    -            "metadata": metadata,
    -            "urls_metadata": urls_metadata,
    -            "baseid": baseid,
    -            "acl": acl,
    -            "authz": authz,
    -            "version": version,
    -            "description": description,
    -            "content_created_date": content_created_date,
    -            "content_updated_date": content_updated_date,
    +            "urls": urls,
    +            "hashes": hashes,
    +            "size": size,
    +            "file_name": file_name,
    +            "metadata": metadata,
    +            "urls_metadata": urls_metadata,
    +            "baseid": baseid,
    +            "acl": acl,
    +            "authz": authz,
    +            "version": version,
    +            "description": description,
    +            "content_created_date": content_created_date,
    +            "content_updated_date": content_updated_date,
             }
             if did:
    -            json["did"] = did
    +            json["did"] = did
             rec = self.client.create(**json)
     
             return rec.to_json()
    @@ -554,12 +554,12 @@

    Source code for gen3.index

             content_created_date=None,
             content_updated_date=None,
         ):
    -        """
    +        """
             Asynchronous function to create a record in indexd.
     
             Args:
                 hashes (dict): {hash type: hash value,}
    -                eg ``hashes={'md5': ab167e49d25b488939b1ede42752458b'}``
    +                eg ``hashes={'md5': ab167e49d25b488939b1ede42752458b'}``
                 size (int): file size metadata associated with a given uuid
                 did (str): provide a UUID for the new indexd to be made
                 urls (list): list of URLs where you can download the UUID
    @@ -576,45 +576,45 @@ 

    Source code for gen3.index

     
             Returns:
                 Document: json representation of an entry in indexd
    -        """
    +        """
             async with aiohttp.ClientSession() as session:
                 if urls is None:
                     urls = []
     
                 json = {
    -                "form": "object",
    -                "hashes": hashes,
    -                "size": size,
    -                "urls": urls or [],
    +                "form": "object",
    +                "hashes": hashes,
    +                "size": size,
    +                "urls": urls or [],
                 }
                 if did:
    -                json["did"] = did
    +                json["did"] = did
                 if file_name:
    -                json["file_name"] = file_name
    +                json["file_name"] = file_name
                 if metadata:
    -                json["metadata"] = metadata
    +                json["metadata"] = metadata
                 if baseid:
    -                json["baseid"] = baseid
    +                json["baseid"] = baseid
                 if acl:
    -                json["acl"] = acl
    +                json["acl"] = acl
                 if urls_metadata:
    -                json["urls_metadata"] = urls_metadata
    +                json["urls_metadata"] = urls_metadata
                 if version:
    -                json["version"] = version
    +                json["version"] = version
                 if authz:
    -                json["authz"] = authz
    +                json["authz"] = authz
                 if description:
    -                json["description"] = description
    +                json["description"] = description
                 if content_created_date:
    -                json["content_created_date"] = content_created_date
    +                json["content_created_date"] = content_created_date
                 if content_updated_date:
    -                json["content_updated_date"] = content_updated_date
    +                json["content_updated_date"] = content_updated_date
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self.client.auth._get_auth_value()}
    +            headers = {"Authorization": self.client.auth._get_auth_value()}
     
                 async with session.post(
    -                f"{self.client.url}/index/",
    +                f"{self.client.url}/index/",
                     json=json,
                     headers=headers,
                     ssl=_ssl,
    @@ -629,29 +629,29 @@ 

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def create_blank(self, uploader, file_name=None):
    -        """
    +        """
     
             Create a blank record
     
             Args:
                 json - json in the format:
                 {
    -                'uploader': type(string)
    -                'file_name': type(string) (optional*)
    +                'uploader': type(string)
    +                'file_name': type(string) (optional*)
                 }
     
    -        """
    -        json = {"uploader": uploader, "file_name": file_name}
    +        """
    +        json = {"uploader": uploader, "file_name": file_name}
             response = self.client._post(
    -            "index/blank",
    -            headers={"content-type": "application/json"},
    +            "index/blank",
    +            headers={"content-type": "application/json"},
                 auth=self.client.auth,
                 data=client.json_dumps(json),
             )
             raise_for_status_and_print_error(response)
             rec = response.json()
     
    -        return self.get_record(rec["did"])
    + return self.get_record(rec["did"])
    @@ -674,7 +674,7 @@

    Source code for gen3.index

             content_created_date=None,
             content_updated_date=None,
         ):
    -        """
    +        """
     
             Add new version for the document associated to the provided uuid
     
    @@ -686,7 +686,7 @@ 

    Source code for gen3.index

             Args:
                 guid: (string): record id
                 hashes (dict): {hash type: hash value,}
    -                eg ``hashes={'md5': ab167e49d25b488939b1ede42752458b'}``
    +                eg ``hashes={'md5': ab167e49d25b488939b1ede42752458b'}``
                 size (int): file size metadata associated with a given uuid
                 did (str): provide a UUID for the new indexd to be made
                 urls (list): list of URLs where you can download the UUID
    @@ -706,38 +706,38 @@ 

    Source code for gen3.index

                   sufficient. Note: it is a good idea to add a version
                   number
     
    -        """
    +        """
             if urls is None:
                 urls = []
             json = {
    -            "urls": urls,
    -            "form": "object",
    -            "hashes": hashes,
    -            "size": size,
    -            "file_name": file_name,
    -            "metadata": metadata,
    -            "urls_metadata": urls_metadata,
    -            "acl": acl,
    -            "authz": authz,
    -            "version": version,
    -            "description": description,
    -            "content_created_date": content_created_date,
    -            "content_updated_date": content_updated_date,
    +            "urls": urls,
    +            "form": "object",
    +            "hashes": hashes,
    +            "size": size,
    +            "file_name": file_name,
    +            "metadata": metadata,
    +            "urls_metadata": urls_metadata,
    +            "acl": acl,
    +            "authz": authz,
    +            "version": version,
    +            "description": description,
    +            "content_created_date": content_created_date,
    +            "content_updated_date": content_updated_date,
             }
             if did:
    -            json["did"] = did
    +            json["did"] = did
             response = self.client._post(
    -            "index",
    +            "index",
                 guid,
    -            headers={"content-type": "application/json"},
    +            headers={"content-type": "application/json"},
                 data=client.json_dumps(json),
                 auth=self.client.auth,
             )
             raise_for_status_and_print_error(response)
             rec = response.json()
     
    -        if rec and "did" in rec:
    -            return self.get_record(rec["did"])
    +        if rec and "did" in rec:
    +            return self.get_record(rec["did"])
             return None
    @@ -745,7 +745,7 @@

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_records(self, dids):
    -        """
    +        """
     
             Get a list of documents given a list of dids
     
    @@ -756,10 +756,10 @@ 

    Source code for gen3.index

             Returns:
                 list: json representing index records
     
    -        """
    +        """
             try:
                 response = self.client._post(
    -                "bulk/documents", json=dids, auth=self.client.auth
    +                "bulk/documents", json=dids, auth=self.client.auth
                 )
             except requests.HTTPError as exception:
                 if exception.response.status_code == 404:
    @@ -776,7 +776,7 @@ 

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def update_blank(self, guid, rev, hashes, size, urls=None, authz=None):
    -        """
    +        """
     
             Update only hashes and size for a blank index
     
    @@ -784,21 +784,21 @@ 

    Source code for gen3.index

                 guid (string): record id
                 rev (string): data revision - simple consistency mechanism
                 hashes (dict): {hash type: hash value,}
    -                eg ``hashes={'md5': ab167e49d25b488939b1ede42752458b'}``
    +                eg ``hashes={'md5': ab167e49d25b488939b1ede42752458b'}``
                 size (int): file size metadata associated with a given uuid
     
    -        """
    -        params = {"rev": rev}
    -        json = {"hashes": hashes, "size": size}
    +        """
    +        params = {"rev": rev}
    +        json = {"hashes": hashes, "size": size}
             if urls:
    -            json["urls"] = urls
    +            json["urls"] = urls
             if authz:
    -            json["authz"] = authz
    +            json["authz"] = authz
     
             response = self.client._put(
    -            "index/blank",
    +            "index/blank",
                 guid,
    -            headers={"content-type": "application/json"},
    +            headers={"content-type": "application/json"},
                 params=params,
                 auth=self.client.auth,
                 data=client.json_dumps(json),
    @@ -806,7 +806,7 @@ 

    Source code for gen3.index

             raise_for_status_and_print_error(response)
             rec = response.json()
     
    -        return self.get_record(rec["did"])
    + return self.get_record(rec["did"])
    @@ -826,7 +826,7 @@

    Source code for gen3.index

             content_created_date=None,
             content_updated_date=None,
         ):
    -        """
    +        """
     
             Update an existing entry in the index
     
    @@ -837,27 +837,27 @@ 

    Source code for gen3.index

                      - index record information that needs to be updated.
                      - can not update size or hash, use new version for that
     
    -        """
    +        """
             updatable_attrs = {
    -            "file_name": file_name,
    -            "urls": urls,
    -            "version": version,
    -            "metadata": metadata,
    -            "acl": acl,
    -            "authz": authz,
    -            "urls_metadata": urls_metadata,
    -            "description": description,
    -            "content_created_date": content_created_date,
    -            "content_updated_date": content_updated_date,
    +            "file_name": file_name,
    +            "urls": urls,
    +            "version": version,
    +            "metadata": metadata,
    +            "acl": acl,
    +            "authz": authz,
    +            "urls_metadata": urls_metadata,
    +            "description": description,
    +            "content_created_date": content_created_date,
    +            "content_updated_date": content_updated_date,
             }
             rec = self.client.get(guid)
             if not rec:
                 raise ValueError(
    -                f"No indexd record found for GUID '{guid}' at '{self.endpoint}'"
    +                f"No indexd record found for GUID '{guid}' at '{self.endpoint}'"
                 )
             for k, v in updatable_attrs.items():
                 if v is not None:
    -                exec(f"rec.{k} = v")
    +                exec(f"rec.{k} = v")
             rec.patch()
             return rec.to_json()
    @@ -881,7 +881,7 @@

    Source code for gen3.index

             content_updated_date=None,
             **kwargs,
         ):
    -        """
    +        """
             Asynchronous function to update a record in indexd.
     
             Args:
    @@ -890,47 +890,47 @@ 

    Source code for gen3.index

                  body: json/dictionary format
                      - index record information that needs to be updated.
                      - can not update size or hash, use new version for that
    -        """
    +        """
             async with aiohttp.ClientSession() as session:
                 updatable_attrs = {
    -                "file_name": file_name,
    -                "urls": urls,
    -                "version": version,
    -                "metadata": metadata,
    -                "acl": acl,
    -                "authz": authz,
    -                "urls_metadata": urls_metadata,
    -                "description": description,
    -                "content_created_date": content_created_date,
    -                "content_updated_date": content_updated_date,
    +                "file_name": file_name,
    +                "urls": urls,
    +                "version": version,
    +                "metadata": metadata,
    +                "acl": acl,
    +                "authz": authz,
    +                "urls_metadata": urls_metadata,
    +                "description": description,
    +                "content_created_date": content_created_date,
    +                "content_updated_date": content_updated_date,
                 }
                 record = await self.async_get_record(guid)
    -            revision = record.get("rev")
    +            revision = record.get("rev")
     
                 for key, value in updatable_attrs.items():
                     if value is not None:
                         record[key] = value
     
    -            del record["created_date"]
    -            del record["rev"]
    -            del record["updated_date"]
    -            del record["version"]
    -            del record["uploader"]
    -            del record["form"]
    -            del record["urls_metadata"]
    -            del record["baseid"]
    -            del record["size"]
    -            del record["hashes"]
    -            del record["did"]
    +            del record["created_date"]
    +            del record["rev"]
    +            del record["updated_date"]
    +            del record["version"]
    +            del record["uploader"]
    +            del record["form"]
    +            del record["urls_metadata"]
    +            del record["baseid"]
    +            del record["size"]
    +            del record["hashes"]
    +            del record["did"]
     
    -            logging.info(f"PUT-ing record: {record}")
    +            logging.info(f"PUT-ing record: {record}")
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self.client.auth._get_auth_value()}
    +            headers = {"Authorization": self.client.auth._get_auth_value()}
     
                 async with session.put(
    -                f"{self.client.url}/index/{guid}?rev={revision}",
    +                f"{self.client.url}/index/{guid}?rev={revision}",
                     json=record,
                     headers=headers,
                     ssl=_ssl,
    @@ -947,7 +947,7 @@ 

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def delete_record(self, guid):
    -        """
    +        """
     
             Delete an entry from the index
     
    @@ -957,7 +957,7 @@ 

    Source code for gen3.index

     
             Returns: Nothing
     
    -        """
    +        """
             rec = self.client.get(guid)
             if rec:
                 rec.delete()
    @@ -970,7 +970,7 @@ 

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def query_urls(self, pattern):
    -        """
    +        """
     
             Query all record URLs for given pattern
     
    @@ -979,8 +979,8 @@ 

    Source code for gen3.index

     
             Returns:
                 List[records]: indexd records with urls matching pattern
    -        """
    -        response = self.client._get(f"/_query/urls/q?include={pattern}")
    +        """
    +        response = self.client._get(f"/_query/urls/q?include={pattern}")
             raise_for_status_and_print_error(response)
             return response.json()
    @@ -989,7 +989,7 @@

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         async def async_query_urls(self, pattern, _ssl=None):
    -        """
    +        """
             Asynchronous function to query urls from indexd.
     
             Args:
    @@ -997,10 +997,10 @@ 

    Source code for gen3.index

     
             Returns:
                 List[records]: indexd records with urls matching pattern
    -        """
    -        url = f"{self.client.url}/_query/urls/q?include={pattern}"
    +        """
    +        url = f"{self.client.url}/_query/urls/q?include={pattern}"
             async with aiohttp.ClientSession() as session:
    -            logging.debug(f"request: {url}")
    +            logging.debug(f"request: {url}")
                 async with session.get(url, ssl=_ssl) as response:
                     raise_for_status_and_print_error(response)
                     response = await response.json()
    @@ -1014,44 +1014,44 @@ 

    Source code for gen3.index

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_valid_guids(self, count=None):
    -        """
    +        """
             Get a list of valid GUIDs without indexing
             Args:
                 count (int): number of GUIDs to request
             Returns:
                 List[str]: list of valid indexd GUIDs
    -        """
    -        url = "/guid/mint"
    +        """
    +        url = "/guid/mint"
             if count:
    -            url += f"?count={count}"
    +            url += f"?count={count}"
     
             response = self.client._get(url)
             response.raise_for_status()
    -        return response.json().get("guids", [])
    + return response.json().get("guids", [])
    [docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) def get_guids_prefix(self): - """ + """ Get the prefix for GUIDs if there is one Returns: str: prefix for this instance - """ - response = self.client._get("/guid/prefix") + """ + response = self.client._get("/guid/prefix") response.raise_for_status() - return response.json().get("prefix")
    + return response.json().get("prefix")
    def _print_func_name(function): - return "{}.{}".format(function.__module__, function.__name__) + return "{}.{}".format(function.__module__, function.__name__) def _print_kwargs(kwargs): - return ", ".join("{}={}".format(k, repr(v)) for k, v in list(kwargs.items())) + return ", ".join("{}={}".format(k, repr(v)) for k, v in list(kwargs.items()))
    diff --git a/docs/_build/html/_modules/gen3/jobs.html b/docs/_build/html/_modules/gen3/jobs.html index ed3448caa..a81656f82 100644 --- a/docs/_build/html/_modules/gen3/jobs.html +++ b/docs/_build/html/_modules/gen3/jobs.html @@ -31,9 +31,9 @@

    Source code for gen3.jobs

    -"""
    -Contains class for interacting with Gen3's Job Dispatching Service(s).
    -"""
    +"""
    +Contains class for interacting with Gen3's Job Dispatching Service(s).
    +"""
     import aiohttp
     import asyncio
     import backoff
    @@ -51,12 +51,12 @@ 

    Source code for gen3.jobs

         raise_for_status_and_print_error,
     )
     
    -# sower's "action" mapping to the relevant job
    -INGEST_METADATA_JOB = "ingest-metadata-manifest"
    -DBGAP_METADATA_JOB = "get-dbgap-metadata"
    -INDEX_MANIFEST_JOB = "index-object-manifest"
    -DOWNLOAD_MANIFEST_JOB = "download-indexd-manifest"
    -MERGE_MANIFEST_JOB = "merge-manifests"
    +# sower's "action" mapping to the relevant job
    +INGEST_METADATA_JOB = "ingest-metadata-manifest"
    +DBGAP_METADATA_JOB = "get-dbgap-metadata"
    +INDEX_MANIFEST_JOB = "index-object-manifest"
    +DOWNLOAD_MANIFEST_JOB = "download-indexd-manifest"
    +MERGE_MANIFEST_JOB = "merge-manifests"
     
     logging = get_logger(__name__)
     
    @@ -64,19 +64,19 @@ 

    Source code for gen3.jobs

     
    [docs] class Gen3Jobs: - """ - A class for interacting with the Gen3's Job Dispatching Service(s). + """ + A class for interacting with the Gen3's Job Dispatching Service(s). Examples: This generates the Gen3Jobs class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page. - >>> auth = Gen3Auth(refresh_file="credentials.json") + >>> auth = Gen3Auth(refresh_file="credentials.json") ... jobs = Gen3Jobs(auth) - """ + """ - def __init__(self, endpoint=None, auth_provider=None, service_location="job"): - """ + def __init__(self, endpoint=None, auth_provider=None, service_location="job"): + """ Initialization for instance of the class to setup basic endpoint info. Args: @@ -84,25 +84,25 @@

    Source code for gen3.jobs

                     token, required for admin endpoints
                 service_location (str, optional): deployment location relative to the
                     endpoint provided
    -        """
    +        """
             # auth_provider legacy interface required endpoint as 1st arg
             auth_provider = auth_provider or endpoint
    -        endpoint = auth_provider.endpoint.strip("/")
    +        endpoint = auth_provider.endpoint.strip("/")
             # if running locally, mds is deployed by itself without a location relative
             # to the commons
    -        if "http://localhost" in endpoint:
    -            service_location = ""
    +        if "http://localhost" in endpoint:
    +            service_location = ""
     
             if not endpoint.endswith(service_location):
    -            endpoint += "/" + service_location
    +            endpoint += "/" + service_location
     
    -        self.endpoint = endpoint.rstrip("/")
    +        self.endpoint = endpoint.rstrip("/")
             self._auth_provider = auth_provider
     
     
    [docs] async def async_run_job_and_wait(self, job_name, job_input, _ssl=None, **kwargs): - """ + """ Asynchronous function to create a job, wait for output, and return. Will sleep in a linear delay until the job is done, starting with 1 second. @@ -113,71 +113,71 @@

    Source code for gen3.jobs

     
             Returns:
                 Dict: Response from the endpoint
    -        """
    +        """
             job_create_response = await self.async_create_job(job_name, job_input)
     
    -        status = {"status": "Running"}
    +        status = {"status": "Running"}
             sleep_time = 3
    -        while status.get("status") == "Running":
    -            logging.info(f"job still running, waiting for {sleep_time} seconds...")
    +        while status.get("status") == "Running":
    +            logging.info(f"job still running, waiting for {sleep_time} seconds...")
                 time.sleep(sleep_time)
                 sleep_time *= 1.5
    -            status = await self.async_get_status(job_create_response.get("uid"))
    -            logging.info(f"{status}")
    +            status = await self.async_get_status(job_create_response.get("uid"))
    +            logging.info(f"{status}")
     
    -        logging.info(f"Job is finished!")
    +        logging.info(f"Job is finished!")
     
    -        if status.get("status") != "Completed":
    -            raise Exception(f"Job status not complete: {status.get('status')}.")
    +        if status.get("status") != "Completed":
    +            raise Exception(f"Job status not complete: {status.get('status')}.")
     
    -        response = await self.async_get_output(job_create_response.get("uid"))
    +        response = await self.async_get_output(job_create_response.get("uid"))
             return response
    [docs] def is_healthy(self): - """ + """ Return if is healthy or not Returns: bool: True if healthy - """ + """ try: response = requests.get( - self.endpoint + "/_status", auth=self._auth_provider + self.endpoint + "/_status", auth=self._auth_provider ) raise_for_status_and_print_error(response) except Exception as exc: logging.error(exc) return False - return response.text == "Healthy"
    + return response.text == "Healthy"
    [docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) def get_version(self): - """ + """ Return the version Returns: str: the version - """ - response = requests.get(self.endpoint + "/_version", auth=self._auth_provider) + """ + response = requests.get(self.endpoint + "/_version", auth=self._auth_provider) raise_for_status_and_print_error(response) - return response.json().get("version")
    + return response.json().get("version")
    [docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) def list_jobs(self): - """ + """ List all jobs - """ - response = requests.get(self.endpoint + "/list", auth=self._auth_provider) + """ + response = requests.get(self.endpoint + "/list", auth=self._auth_provider) raise_for_status_and_print_error(response) return response.json()
    @@ -186,7 +186,7 @@

    Source code for gen3.jobs

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def create_job(self, job_name, job_input):
    -        """
    +        """
             Create a job with given name and input
     
             Args:
    @@ -195,10 +195,10 @@ 

    Source code for gen3.jobs

     
             Returns:
                 Dict: Response from the endpoint
    -        """
    -        data = {"action": job_name, "input": job_input}
    +        """
    +        data = {"action": job_name, "input": job_input}
             response = requests.post(
    -            self.endpoint + "/dispatch", json=data, auth=self._auth_provider
    +            self.endpoint + "/dispatch", json=data, auth=self._auth_provider
             )
             raise_for_status_and_print_error(response)
             return response.json()
    @@ -207,14 +207,14 @@

    Source code for gen3.jobs

         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         async def async_create_job(self, job_name, job_input, _ssl=None, **kwargs):
             async with aiohttp.ClientSession() as session:
    -            url = self.endpoint + f"/dispatch"
    +            url = self.endpoint + f"/dispatch"
                 url_with_params = append_query_params(url, **kwargs)
     
    -            data = json.dumps({"action": job_name, "input": job_input})
    +            data = json.dumps({"action": job_name, "input": job_input})
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self._auth_provider._get_auth_value()}
    +            headers = {"Authorization": self._auth_provider._get_auth_value()}
     
                 async with session.post(
                     url_with_params, data=data, headers=headers, ssl=_ssl
    @@ -227,11 +227,11 @@ 

    Source code for gen3.jobs

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_status(self, job_id):
    -        """
    +        """
             Get the status of a previously created job
    -        """
    +        """
             response = requests.get(
    -            self.endpoint + f"/status?UID={job_id}", auth=self._auth_provider
    +            self.endpoint + f"/status?UID={job_id}", auth=self._auth_provider
             )
             raise_for_status_and_print_error(response)
             return response.json()
    @@ -240,12 +240,12 @@

    Source code for gen3.jobs

         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         async def async_get_status(self, job_id, _ssl=None, **kwargs):
             async with aiohttp.ClientSession() as session:
    -            url = self.endpoint + f"/status?UID={job_id}"
    +            url = self.endpoint + f"/status?UID={job_id}"
                 url_with_params = append_query_params(url, **kwargs)
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self._auth_provider._get_auth_value()}
    +            headers = {"Authorization": self._auth_provider._get_auth_value()}
     
                 async with session.get(
                     url_with_params, headers=headers, ssl=_ssl
    @@ -258,11 +258,11 @@ 

    Source code for gen3.jobs

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_output(self, job_id):
    -        """
    +        """
             Get the output of a previously completed job
    -        """
    +        """
             response = requests.get(
    -            self.endpoint + f"/output?UID={job_id}", auth=self._auth_provider
    +            self.endpoint + f"/output?UID={job_id}", auth=self._auth_provider
             )
             raise_for_status_and_print_error(response)
             return response.json()
    @@ -271,12 +271,12 @@

    Source code for gen3.jobs

         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         async def async_get_output(self, job_id, _ssl=None, **kwargs):
             async with aiohttp.ClientSession() as session:
    -            url = self.endpoint + f"/output?UID={job_id}"
    +            url = self.endpoint + f"/output?UID={job_id}"
                 url_with_params = append_query_params(url, **kwargs)
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self._auth_provider._get_auth_value()}
    +            headers = {"Authorization": self._auth_provider._get_auth_value()}
     
                 async with session.get(
                     url_with_params, headers=headers, ssl=_ssl
    diff --git a/docs/_build/html/_modules/gen3/metadata.html b/docs/_build/html/_modules/gen3/metadata.html
    index cb785a927..7e1dc353c 100644
    --- a/docs/_build/html/_modules/gen3/metadata.html
    +++ b/docs/_build/html/_modules/gen3/metadata.html
    @@ -31,9 +31,9 @@
               

    Source code for gen3.metadata

    -"""
    -Contains class for interacting with Gen3's Metadata Service.
    -"""
    +"""
    +Contains class for interacting with Gen3's Metadata Service.
    +"""
     import aiohttp
     import backoff
     from datetime import datetime
    @@ -66,24 +66,24 @@ 

    Source code for gen3.metadata

     logging = get_logger(__name__)
     
     
    -PACKAGE_CONTENTS_STANDARD_KEY = "package_contents"
    +PACKAGE_CONTENTS_STANDARD_KEY = "package_contents"
     PACKAGE_CONTENTS_SCHEMA = {
    -    "type": "array",
    -    "items": {
    -        "type": "object",
    -        "properties": {
    -            "file_name": {
    -                "type": "string",
    +    "type": "array",
    +    "items": {
    +        "type": "object",
    +        "properties": {
    +            "file_name": {
    +                "type": "string",
                 },
    -            "size": {
    -                "type": "integer",
    +            "size": {
    +                "type": "integer",
                 },
    -            "hashes": {
    -                "type": "object",
    +            "hashes": {
    +                "type": "object",
                 },
             },
    -        "required": ["file_name"],
    -        "additionalProperties": True,
    +        "required": ["file_name"],
    +        "additionalProperties": True,
         },
     }
     
    @@ -91,29 +91,29 @@ 

    Source code for gen3.metadata

     
    [docs] class Gen3Metadata: - """ + """ A class for interacting with the Gen3 Metadata services. Examples: This generates the Gen3Metadata class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page. - >>> auth = Gen3Auth(refresh_file="credentials.json") + >>> auth = Gen3Auth(refresh_file="credentials.json") ... metadata = Gen3Metadata(auth) Attributes: endpoint (str): public endpoint for reading/querying metadata - only necessary if auth_provider not provided auth_provider (Gen3Auth): auth manager - """ + """ def __init__( self, endpoint=None, auth_provider=None, - service_location="mds", - admin_endpoint_suffix="-admin", + service_location="mds", + admin_endpoint_suffix="-admin", ): - """ + """ Initialization for instance of the class to setup basic endpoint info. Args: @@ -122,59 +122,59 @@

    Source code for gen3.metadata

                     token, required for admin endpoints
                 service_location (str, optional): deployment location relative to the
                     endpoint provided
    -        """
    +        """
             # legacy interface required endpoint as 1st arg
             if endpoint and isinstance(endpoint, Gen3Auth):
                 auth_provider = endpoint
                 endpoint = None
             if auth_provider and isinstance(auth_provider, Gen3Auth):
                 endpoint = auth_provider.endpoint
    -        endpoint = endpoint.strip("/")
    +        endpoint = endpoint.strip("/")
             # if running locally, mds is deployed by itself without a location relative
             # to the commons
    -        if "http://localhost" in endpoint:
    -            service_location = ""
    -            admin_endpoint_suffix = ""
    +        if "http://localhost" in endpoint:
    +            service_location = ""
    +            admin_endpoint_suffix = ""
     
             if not endpoint.endswith(service_location):
    -            endpoint += "/" + service_location
    +            endpoint += "/" + service_location
     
    -        self.endpoint = endpoint.rstrip("/")
    -        self.admin_endpoint = endpoint.rstrip("/") + admin_endpoint_suffix
    +        self.endpoint = endpoint.rstrip("/")
    +        self.admin_endpoint = endpoint.rstrip("/") + admin_endpoint_suffix
             self._auth_provider = auth_provider
     
     
    [docs] def is_healthy(self): - """ + """ Return if is healthy or not Returns: bool: True if healthy - """ + """ try: response = requests.get( - self.endpoint + "/_status", auth=self._auth_provider + self.endpoint + "/_status", auth=self._auth_provider ) response.raise_for_status() except Exception as exc: logging.error(exc) return False - return response.json().get("status") == "OK"
    + return response.json().get("status") == "OK"
    [docs] @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) def get_version(self): - """ + """ Return the version Returns: str: the version - """ - response = requests.get(self.endpoint + "/version", auth=self._auth_provider) + """ + response = requests.get(self.endpoint + "/version", auth=self._auth_provider) response.raise_for_status() return response.text
    @@ -183,14 +183,14 @@

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def get_index_key_paths(self):
    -        """
    +        """
             List all the metadata key paths indexed in the database.
     
             Returns:
                 List: list of metadata key paths
    -        """
    +        """
             response = requests.get(
    -            self.admin_endpoint + "/metadata_index", auth=self._auth_provider
    +            self.admin_endpoint + "/metadata_index", auth=self._auth_provider
             )
             response.raise_for_status()
             return response.json()
    @@ -200,14 +200,14 @@

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def create_index_key_path(self, path):
    -        """
    +        """
             Create a metadata key path indexed in the database.
     
             Args:
                 path (str): metadata key path
    -        """
    +        """
             response = requests.post(
    -            self.admin_endpoint + f"/metadata_index/{path}", auth=self._auth_provider
    +            self.admin_endpoint + f"/metadata_index/{path}", auth=self._auth_provider
             )
             response.raise_for_status()
             return response.json()
    @@ -217,14 +217,14 @@

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def delete_index_key_path(self, path):
    -        """
    +        """
             List all the metadata key paths indexed in the database.
     
             Args:
                 path (str): metadata key path
    -        """
    +        """
             response = requests.delete(
    -            self.admin_endpoint + f"/metadata_index/{path}", auth=self._auth_provider
    +            self.admin_endpoint + f"/metadata_index/{path}", auth=self._auth_provider
             )
             response.raise_for_status()
             return response
    @@ -242,38 +242,38 @@

    Source code for gen3.metadata

             use_agg_mds=False,
             **kwargs,
         ):
    -        """
    +        """
             Query the metadata given a query.
     
             Query format is based off the logic used in the service:
    -            '''
    +            '''
                 Without filters, this will return all data. Add filters as query strings like this:
     
                 GET /metadata?a=1&b=2
     
             This will match all records that have metadata containing all of:
    -            {"a": 1, "b": 2}
    +            {"a": 1, "b": 2}
                 The values are always treated as strings for filtering. Nesting is supported:
     
                 GET /metadata?a.b.c=3
     
             Matching records containing:
    -            {"a": {"b": {"c": 3}}}
    +            {"a": {"b": {"c": 3}}}
                 Providing the same key with more than one value filters records whose value of the given key matches any of the given values. But values of different keys must all match. For example:
     
                 GET /metadata?a.b.c=3&a.b.c=33&a.b.d=4
     
             Matches these:
    -            {"a": {"b": {"c": 3, "d": 4}}}
    -            {"a": {"b": {"c": 33, "d": 4}}}
    -            {"a": {"b": {"c": "3", "d": 4, "e": 5}}}
    -            But won't match these:
    +            {"a": {"b": {"c": 3, "d": 4}}}
    +            {"a": {"b": {"c": 33, "d": 4}}}
    +            {"a": {"b": {"c": "3", "d": 4, "e": 5}}}
    +            But won't match these:
     
    -            {"a": {"b": {"c": 3}}}
    -            {"a": {"b": {"c": 3, "d": 5}}}
    -            {"a": {"b": {"d": 5}}}
    -            {"a": {"b": {"c": "333", "d": 4}}}
    -            '''
    +            {"a": {"b": {"c": 3}}}
    +            {"a": {"b": {"c": 3, "d": 5}}}
    +            {"a": {"b": {"d": 5}}}
    +            {"a": {"b": {"c": "333", "d": 4}}}
    +            '''
     
             Args:
                 query (str): mds query as defined by the metadata api
    @@ -286,14 +286,14 @@ 

    Source code for gen3.metadata

                     OR if return_full_metadata=True
                 Dict{guid: {metadata}}: Dictionary with GUIDs as keys and associated
                     metadata JSON blobs as values
    -        """
    +        """
     
    -        url = self.endpoint + f"/metadata?{query}"
    +        url = self.endpoint + f"/metadata?{query}"
     
             url_with_params = append_query_params(
                 url, data=return_full_metadata, limit=limit, offset=offset, **kwargs
             )
    -        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"hitting: {url_with_params}")
             response = requests.get(url_with_params, auth=self._auth_provider)
             response.raise_for_status()
     
    @@ -304,7 +304,7 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         async def async_get(self, guid, _ssl=None, **kwargs):
    -        """
    +        """
             Asynchronous function to get metadata
     
             Args:
    @@ -313,12 +313,12 @@ 

    Source code for gen3.metadata

     
             Returns:
                 Dict: metadata for given guid
    -        """
    +        """
             async with aiohttp.ClientSession() as session:
    -            url = self.endpoint + f"/metadata/{guid}"
    +            url = self.endpoint + f"/metadata/{guid}"
                 url_with_params = append_query_params(url, **kwargs)
     
    -            logging.debug(f"hitting: {url_with_params}")
    +            logging.debug(f"hitting: {url_with_params}")
     
                 async with session.get(url_with_params, ssl=_ssl) as response:
                     response.raise_for_status()
    @@ -331,16 +331,16 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         def get(self, guid, **kwargs):
    -        """
    +        """
             Get the metadata associated with the guid
             Args:
                 guid (str): guid to use
             Returns:
                 Dict: metadata for given guid
    -        """
    -        url = self.endpoint + f"/metadata/{guid}"
    +        """
    +        url = self.endpoint + f"/metadata/{guid}"
             url_with_params = append_query_params(url, **kwargs)
    -        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"hitting: {url_with_params}")
             response = requests.get(url_with_params, auth=self._auth_provider)
     
             response.raise_for_status()
    @@ -352,30 +352,30 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def batch_create(self, metadata_list, overwrite=True, **kwargs):
    -        """
    +        """
             Create the list of metadata associated with the list of guids
     
             Args:
    -            metadata_list (List[Dict{"guid": "", "data": {}}]): list of metadata
    -                objects in a specific format. Expects a dict with "guid" and "data"
    -                fields where "data" is another JSON blob to add to the mds
    +            metadata_list (List[Dict{"guid": "", "data": {}}]): list of metadata
    +                objects in a specific format. Expects a dict with "guid" and "data"
    +                fields where "data" is another JSON blob to add to the mds
                 overwrite (bool, optional): whether or not to overwrite existing data
    -        """
    -        url = self.admin_endpoint + f"/metadata"
    +        """
    +        url = self.admin_endpoint + f"/metadata"
     
             if len(metadata_list) > 1 and (
    -            "guid" not in metadata_list[0] and "data" not in metadata_list[0]
    +            "guid" not in metadata_list[0] and "data" not in metadata_list[0]
             ):
                 logging.warning(
    -                "it looks like your metadata list for bulk create is malformed. "
    -                "the expected format is a list of dicts that have 2 keys: 'guid' "
    -                "and 'data', where 'guid' is a string and 'data' is another dict. "
    -                f"The first element doesn't match that pattern: {metadata_list[0]}"
    +                "it looks like your metadata list for bulk create is malformed. "
    +                "the expected format is a list of dicts that have 2 keys: 'guid' "
    +                "and 'data', where 'guid' is a string and 'data' is another dict. "
    +                f"The first element doesn't match that pattern: {metadata_list[0]}"
                 )
     
             url_with_params = append_query_params(url, overwrite=overwrite, **kwargs)
    -        logging.debug(f"hitting: {url_with_params}")
    -        logging.debug(f"data: {metadata_list}")
    +        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"data: {metadata_list}")
             response = requests.post(
                 url_with_params, json=metadata_list, auth=self._auth_provider
             )
    @@ -388,7 +388,7 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         def create(self, guid, metadata, aliases=None, overwrite=False, **kwargs):
    -        """
    +        """
             Create the metadata associated with the guid
     
             Args:
    @@ -396,14 +396,14 @@ 

    Source code for gen3.metadata

                 metadata (Dict): dictionary representing what will end up a JSON blob
                     attached to the provided GUID as metadata
                 overwrite (bool, optional): whether or not to overwrite existing data
    -        """
    +        """
             aliases = aliases or []
     
    -        url = self.admin_endpoint + f"/metadata/{guid}"
    +        url = self.admin_endpoint + f"/metadata/{guid}"
     
             url_with_params = append_query_params(url, overwrite=overwrite, **kwargs)
    -        logging.debug(f"hitting: {url_with_params}")
    -        logging.debug(f"data: {metadata}")
    +        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"data: {metadata}")
             response = requests.post(
                 url_with_params, json=metadata, auth=self._auth_provider
             )
    @@ -414,10 +414,10 @@ 

    Source code for gen3.metadata

                     self.create_aliases(guid=guid, aliases=aliases, merge=overwrite)
                 except Exception:
                     logging.error(
    -                    "Error while attempting to create aliases: "
    -                    f"'{aliases}' to GUID: '{guid}' with merge={overwrite}. "
    -                    "GUID metadata record was created successfully and "
    -                    "will NOT be deleted."
    +                    "Error while attempting to create aliases: "
    +                    f"'{aliases}' to GUID: '{guid}' with merge={overwrite}. "
    +                    "GUID metadata record was created successfully and "
    +                    "will NOT be deleted."
                     )
     
             return response.json()
    @@ -435,7 +435,7 @@

    Source code for gen3.metadata

             _ssl=None,
             **kwargs,
         ):
    -        """
    +        """
             Asynchronous function to create metadata
     
             Args:
    @@ -444,19 +444,19 @@ 

    Source code for gen3.metadata

                     attached to the provided GUID as metadata
                 overwrite (bool, optional): whether or not to overwrite existing data
                 _ssl (None, optional): whether or not to use ssl
    -        """
    +        """
             aliases = aliases or []
     
             async with aiohttp.ClientSession() as session:
    -            url = self.admin_endpoint + f"/metadata/{guid}"
    +            url = self.admin_endpoint + f"/metadata/{guid}"
                 url_with_params = append_query_params(url, overwrite=overwrite, **kwargs)
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self._auth_provider._get_auth_value()}
    +            headers = {"Authorization": self._auth_provider._get_auth_value()}
     
    -            logging.debug(f"hitting: {url_with_params}")
    -            logging.debug(f"data: {metadata}")
    +            logging.debug(f"hitting: {url_with_params}")
    +            logging.debug(f"data: {metadata}")
                 async with session.post(
                     url_with_params, json=metadata, headers=headers, ssl=_ssl
                 ) as response:
    @@ -464,17 +464,17 @@ 

    Source code for gen3.metadata

                     response = await response.json()
     
                 if aliases:
    -                logging.info(f"creating aliases: {aliases}")
    +                logging.info(f"creating aliases: {aliases}")
                     try:
                         await self.async_create_aliases(
                             guid=guid, aliases=aliases, _ssl=_ssl
                         )
                     except Exception:
                         logging.error(
    -                        "Error while attempting to create aliases: "
    -                        f"'{aliases}' to GUID: '{guid}'. "
    -                        "GUID metadata record was created successfully and "
    -                        "will NOT be deleted."
    +                        "Error while attempting to create aliases: "
    +                        f"'{aliases}' to GUID: '{guid}'. "
    +                        "GUID metadata record was created successfully and "
    +                        "will NOT be deleted."
                         )
     
             return response
    @@ -484,21 +484,21 @@

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def update(self, guid, metadata, aliases=None, merge=False, **kwargs):
    -        """
    +        """
             Update the metadata associated with the guid
     
             Args:
                 guid (str): guid to use
                 metadata (Dict): dictionary representing what will end up a JSON blob
                     attached to the provided GUID as metadata
    -        """
    +        """
             aliases = aliases or []
     
    -        url = self.admin_endpoint + f"/metadata/{guid}"
    +        url = self.admin_endpoint + f"/metadata/{guid}"
     
             url_with_params = append_query_params(url, **kwargs)
    -        logging.debug(f"hitting: {url_with_params}")
    -        logging.debug(f"data: {metadata}")
    +        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"data: {metadata}")
             response = requests.put(
                 url_with_params, json=metadata, auth=self._auth_provider
             )
    @@ -509,10 +509,10 @@ 

    Source code for gen3.metadata

                     self.update_aliases(guid=guid, aliases=aliases, merge=merge)
                 except Exception:
                     logging.error(
    -                    "Error while attempting to update aliases: "
    -                    f"'{aliases}' to GUID: '{guid}'. "
    -                    "GUID metadata record was created successfully and "
    -                    "will NOT be deleted."
    +                    "Error while attempting to update aliases: "
    +                    f"'{aliases}' to GUID: '{guid}'. "
    +                    "GUID metadata record was created successfully and "
    +                    "will NOT be deleted."
                     )
     
             return response.json()
    @@ -524,7 +524,7 @@

    Source code for gen3.metadata

         async def async_update(
             self, guid, metadata, aliases=None, merge=False, _ssl=None, **kwargs
         ):
    -        """
    +        """
             Asynchronous function to update metadata
     
             Args:
    @@ -536,16 +536,16 @@ 

    Source code for gen3.metadata

                   with existing values
                 _ssl (None, optional): whether or not to use ssl
                 **kwargs: Description
    -        """
    +        """
             aliases = aliases or []
     
             async with aiohttp.ClientSession() as session:
    -            url = self.admin_endpoint + f"/metadata/{guid}"
    +            url = self.admin_endpoint + f"/metadata/{guid}"
                 url_with_params = append_query_params(url, merge=merge, **kwargs)
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self._auth_provider._get_auth_value()}
    +            headers = {"Authorization": self._auth_provider._get_auth_value()}
     
                 async with session.put(
                     url_with_params, json=metadata, headers=headers, ssl=_ssl
    @@ -560,10 +560,10 @@ 

    Source code for gen3.metadata

                         )
                     except Exception:
                         logging.error(
    -                        "Error while attempting to update aliases: "
    -                        f"'{aliases}' to GUID: '{guid}' with merge={merge}. "
    -                        "GUID metadata record was created successfully and "
    -                        "will NOT be deleted."
    +                        "Error while attempting to update aliases: "
    +                        f"'{aliases}' to GUID: '{guid}' with merge={merge}. "
    +                        "GUID metadata record was created successfully and "
    +                        "will NOT be deleted."
                         )
     
             return response
    @@ -573,16 +573,16 @@

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS)
         def delete(self, guid, **kwargs):
    -        """
    +        """
             Delete the metadata associated with the guid
     
             Args:
                 guid (str): guid to use
    -        """
    -        url = self.admin_endpoint + f"/metadata/{guid}"
    +        """
    +        url = self.admin_endpoint + f"/metadata/{guid}"
     
             url_with_params = append_query_params(url, **kwargs)
    -        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"hitting: {url_with_params}")
             response = requests.delete(url_with_params, auth=self._auth_provider)
             response.raise_for_status()
     
    @@ -597,7 +597,7 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         def get_aliases(self, guid, **kwargs):
    -        """
    +        """
             Get Aliases for the given guid
     
             Args:
    @@ -606,11 +606,11 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to get aliases
    -        """
    -        url = self.endpoint + f"/metadata/{guid}/aliases"
    +        """
    +        url = self.endpoint + f"/metadata/{guid}/aliases"
             url_with_params = append_query_params(url, **kwargs)
     
    -        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"hitting: {url_with_params}")
             response = requests.get(url_with_params, auth=self._auth_provider)
             response.raise_for_status()
     
    @@ -621,7 +621,7 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         async def async_get_aliases(self, guid, _ssl=None, **kwargs):
    -        """
    +        """
             Asyncronously get Aliases for the given guid
     
             Args:
    @@ -631,16 +631,16 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to get aliases
    -        """
    +        """
             async with aiohttp.ClientSession() as session:
    -            url = self.endpoint + f"/metadata/{guid}/aliases"
    +            url = self.endpoint + f"/metadata/{guid}/aliases"
                 url_with_params = append_query_params(url, **kwargs)
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self._auth_provider._get_auth_value()}
    +            headers = {"Authorization": self._auth_provider._get_auth_value()}
     
    -            logging.debug(f"hitting: {url_with_params}")
    +            logging.debug(f"hitting: {url_with_params}")
                 async with session.get(
                     url_with_params, headers=headers, ssl=_ssl
                 ) as response:
    @@ -651,7 +651,7 @@ 

    Source code for gen3.metadata

     
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         def delete_alias(self, guid, alias, **kwargs):
    -        """
    +        """
             Delete single Alias for the given guid
     
             Args:
    @@ -660,11 +660,11 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to delete aliases
    -        """
    -        url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}"
    +        """
    +        url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}"
             url_with_params = append_query_params(url, **kwargs)
     
    -        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"hitting: {url_with_params}")
             response = requests.delete(url_with_params, auth=self._auth_provider)
             response.raise_for_status()
     
    @@ -672,7 +672,7 @@ 

    Source code for gen3.metadata

     
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         async def async_delete_alias(self, guid, alias, _ssl=None, **kwargs):
    -        """
    +        """
             Asyncronously delete single Aliases for the given guid
     
             Args:
    @@ -682,16 +682,16 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to delete aliases
    -        """
    +        """
             async with aiohttp.ClientSession() as session:
    -            url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}"
    +            url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}"
                 url_with_params = append_query_params(url, **kwargs)
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self._auth_provider._get_auth_value()}
    +            headers = {"Authorization": self._auth_provider._get_auth_value()}
     
    -            logging.debug(f"hitting: {url_with_params}")
    +            logging.debug(f"hitting: {url_with_params}")
                 async with session.delete(
                     url_with_params, headers=headers, ssl=_ssl
                 ) as response:
    @@ -703,7 +703,7 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         def create_aliases(self, guid, aliases, **kwargs):
    -        """
    +        """
             Create Aliases for the given guid
     
             Args:
    @@ -713,14 +713,14 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to create aliases
    -        """
    -        url = self.admin_endpoint + f"/metadata/{guid}/aliases"
    +        """
    +        url = self.admin_endpoint + f"/metadata/{guid}/aliases"
             url_with_params = append_query_params(url, **kwargs)
     
    -        data = {"aliases": aliases}
    +        data = {"aliases": aliases}
     
    -        logging.debug(f"hitting: {url_with_params}")
    -        logging.debug(f"data: {data}")
    +        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"data: {data}")
             response = requests.post(url_with_params, json=data, auth=self._auth_provider)
             response.raise_for_status()
     
    @@ -731,7 +731,7 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         async def async_create_aliases(self, guid, aliases, _ssl=None, **kwargs):
    -        """
    +        """
             Asyncronously create Aliases for the given guid
     
             Args:
    @@ -742,19 +742,19 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to create aliases
    -        """
    +        """
             async with aiohttp.ClientSession() as session:
    -            url = self.admin_endpoint + f"/metadata/{guid}/aliases"
    +            url = self.admin_endpoint + f"/metadata/{guid}/aliases"
                 url_with_params = append_query_params(url, **kwargs)
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self._auth_provider._get_auth_value()}
    +            headers = {"Authorization": self._auth_provider._get_auth_value()}
     
    -            data = {"aliases": aliases}
    +            data = {"aliases": aliases}
     
    -            logging.debug(f"hitting: {url_with_params}")
    -            logging.debug(f"data: {data}")
    +            logging.debug(f"hitting: {url_with_params}")
    +            logging.debug(f"data: {data}")
                 async with session.post(
                     url_with_params, json=data, headers=headers, ssl=_ssl
                 ) as response:
    @@ -766,7 +766,7 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         def update_aliases(self, guid, aliases, merge=False, **kwargs):
    -        """
    +        """
             Update Aliases for the given guid
     
             Args:
    @@ -777,14 +777,14 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to update aliases
    -        """
    -        url = self.admin_endpoint + f"/metadata/{guid}/aliases"
    +        """
    +        url = self.admin_endpoint + f"/metadata/{guid}/aliases"
             url_with_params = append_query_params(url, merge=merge, **kwargs)
     
    -        data = {"aliases": aliases}
    +        data = {"aliases": aliases}
     
    -        logging.debug(f"hitting: {url_with_params}")
    -        logging.debug(f"data: {data}")
    +        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"data: {data}")
             response = requests.put(url_with_params, json=data, auth=self._auth_provider)
             response.raise_for_status()
     
    @@ -797,7 +797,7 @@ 

    Source code for gen3.metadata

         async def async_update_aliases(
             self, guid, aliases, merge=False, _ssl=None, **kwargs
         ):
    -        """
    +        """
             Asyncronously update Aliases for the given guid
     
             Args:
    @@ -809,19 +809,19 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to update aliases
    -        """
    +        """
             async with aiohttp.ClientSession() as session:
    -            url = self.admin_endpoint + f"/metadata/{guid}/aliases"
    +            url = self.admin_endpoint + f"/metadata/{guid}/aliases"
                 url_with_params = append_query_params(url, merge=merge, **kwargs)
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self._auth_provider._get_auth_value()}
    +            headers = {"Authorization": self._auth_provider._get_auth_value()}
     
    -            data = {"aliases": aliases}
    +            data = {"aliases": aliases}
     
    -            logging.debug(f"hitting: {url_with_params}")
    -            logging.debug(f"data: {data}")
    +            logging.debug(f"hitting: {url_with_params}")
    +            logging.debug(f"data: {data}")
                 async with session.put(
                     url_with_params, json=data, headers=headers, ssl=_ssl
                 ) as response:
    @@ -834,7 +834,7 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         def delete_aliases(self, guid, **kwargs):
    -        """
    +        """
             Delete all Aliases for the given guid
     
             Args:
    @@ -843,11 +843,11 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to delete aliases
    -        """
    -        url = self.admin_endpoint + f"/metadata/{guid}/aliases"
    +        """
    +        url = self.admin_endpoint + f"/metadata/{guid}/aliases"
             url_with_params = append_query_params(url, **kwargs)
     
    -        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"hitting: {url_with_params}")
             response = requests.delete(url_with_params, auth=self._auth_provider)
             response.raise_for_status()
     
    @@ -858,7 +858,7 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         async def async_delete_aliases(self, guid, _ssl=None, **kwargs):
    -        """
    +        """
             Asyncronously delete all Aliases for the given guid
     
             Args:
    @@ -868,16 +868,16 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to delete aliases
    -        """
    +        """
             async with aiohttp.ClientSession() as session:
    -            url = self.admin_endpoint + f"/metadata/{guid}/aliases"
    +            url = self.admin_endpoint + f"/metadata/{guid}/aliases"
                 url_with_params = append_query_params(url, **kwargs)
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self._auth_provider._get_auth_value()}
    +            headers = {"Authorization": self._auth_provider._get_auth_value()}
     
    -            logging.debug(f"hitting: {url_with_params}")
    +            logging.debug(f"hitting: {url_with_params}")
                 async with session.delete(
                     url_with_params, headers=headers, ssl=_ssl
                 ) as response:
    @@ -890,7 +890,7 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         def delete_alias(self, guid, alias, **kwargs):
    -        """
    +        """
             Delete single Alias for the given guid
     
             Args:
    @@ -900,11 +900,11 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to delete aliases
    -        """
    -        url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}"
    +        """
    +        url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}"
             url_with_params = append_query_params(url, **kwargs)
     
    -        logging.debug(f"hitting: {url_with_params}")
    +        logging.debug(f"hitting: {url_with_params}")
             response = requests.delete(url_with_params, auth=self._auth_provider)
             response.raise_for_status()
     
    @@ -915,7 +915,7 @@ 

    Source code for gen3.metadata

     [docs]
         @backoff.on_exception(backoff.expo, Exception, **BACKOFF_NO_LOG_IF_NOT_RETRIED)
         async def async_delete_alias(self, guid, alias, _ssl=None, **kwargs):
    -        """
    +        """
             Asyncronously delete single Aliases for the given guid
     
             Args:
    @@ -926,16 +926,16 @@ 

    Source code for gen3.metadata

     
             Returns:
                 requests.Response: response from the request to delete aliases
    -        """
    +        """
             async with aiohttp.ClientSession() as session:
    -            url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}"
    +            url = self.admin_endpoint + f"/metadata/{guid}/aliases/{alias}"
                 url_with_params = append_query_params(url, **kwargs)
     
                 # aiohttp only allows basic auth with their built in auth, so we
                 # need to manually add JWT auth header
    -            headers = {"Authorization": self._auth_provider._get_auth_value()}
    +            headers = {"Authorization": self._auth_provider._get_auth_value()}
     
    -            logging.debug(f"hitting: {url_with_params}")
    +            logging.debug(f"hitting: {url_with_params}")
                 async with session.delete(
                     url_with_params, headers=headers, ssl=_ssl
                 ) as response:
    @@ -947,11 +947,11 @@ 

    Source code for gen3.metadata

         def _prepare_metadata(
             self, metadata, indexd_doc, force_metadata_columns_even_if_empty
         ):
    -        """
    +        """
             Validate and generate the provided metadata for submission to the metadata
             service.
     
    -        If the record is of type "package", also prepare package metadata.
    +        If the record is of type "package", also prepare package metadata.
     
             Args:
                 metadata (dict): metadata provided by the submitter
    @@ -960,13 +960,13 @@ 

    Source code for gen3.metadata

     
             Returns:
                 dict: metadata ready to be submitted to the metadata service
    -        """
    +        """
     
             def _extract_non_indexd_metadata(metadata):
    -            """
    -            Get the "additional metadata": metadata that was provided but is
    +            """
    +            Get the "additional metadata": metadata that was provided but is
                 not stored in indexd, so should be stored in the metadata service.
    -            """
    +            """
                 return {
                     k: v
                     for k, v in metadata.items()
    @@ -987,14 +987,14 @@ 

    Source code for gen3.metadata

             valid = True
     
             # validate package columns
    -        record_type = to_submit.pop(RECORD_TYPE_STANDARD_KEY, "").strip().lower()
    +        record_type = to_submit.pop(RECORD_TYPE_STANDARD_KEY, "").strip().lower()
             package_contents = to_submit.pop(PACKAGE_CONTENTS_STANDARD_KEY, None)
    -        if record_type == "package":
    +        if record_type == "package":
                 if package_contents:
                     package_contents = json.loads(package_contents)
                     if not _verify_schema(package_contents, PACKAGE_CONTENTS_SCHEMA):
                         logging.error(
    -                        f"ERROR: {package_contents} is not in package contents format"
    +                        f"ERROR: {package_contents} is not in package contents format"
                         )
                         valid = False
                 # generate package metadata
    @@ -1009,19 +1009,19 @@ 

    Source code for gen3.metadata

                 to_submit.update(package_metadata)
             elif package_contents:
                 logging.error(
    -                f"ERROR: tried to set '{PACKAGE_CONTENTS_STANDARD_KEY}' for a non-package row. Ignoring '{PACKAGE_CONTENTS_STANDARD_KEY}'. Set '{RECORD_TYPE_STANDARD_KEY}' to 'package' to create packages."
    +                f"ERROR: tried to set '{PACKAGE_CONTENTS_STANDARD_KEY}' for a non-package row. Ignoring '{PACKAGE_CONTENTS_STANDARD_KEY}'. Set '{RECORD_TYPE_STANDARD_KEY}' to 'package' to create packages."
                 )
                 valid = False
     
             if not valid:
    -            raise Exception(f"Metadata is not valid: {metadata}")
    +            raise Exception(f"Metadata is not valid: {metadata}")
     
             if not force_metadata_columns_even_if_empty:
    -            # remove any empty columns if we're not being forced to include them
    +            # remove any empty columns if we're not being forced to include them
                 to_submit = {
                     key: value
                     for key, value in to_submit.items()
    -                if value is not None and value != ""
    +                if value is not None and value != ""
                 }
     
             return to_submit
    @@ -1029,18 +1029,18 @@ 

    Source code for gen3.metadata

         def _get_package_metadata(
             self, submitted_metadata, file_name, file_size, hashes, urls, contents
         ):
    -        """
    +        """
             The MDS Objects API currently expects files that have not been
             uploaded yet. For files we only needs to index, not upload, create
             object records manually by generating the expected object fields.
             TODO: update the MDS objects API to not create upload URLs if the
             relevant data is provided.
    -        """
    +        """
     
             def _get_filename_from_urls(submitted_metadata, urls):
    -            file_name = ""
    +            file_name = ""
                 if not urls:
    -                logging.warning(f"No URLs provided for: {submitted_metadata}")
    +                logging.warning(f"No URLs provided for: {submitted_metadata}")
                 for url in urls:
                     _file_name = os.path.basename(url)
                     if not file_name:
    @@ -1048,7 +1048,7 @@ 

    Source code for gen3.metadata

                     else:
                         if file_name != _file_name:
                             logging.warning(
    -                            f"Received multiple URLs with different file names; will use the first URL (file name '{file_name}'): {submitted_metadata}"
    +                            f"Received multiple URLs with different file names; will use the first URL (file name '{file_name}'): {submitted_metadata}"
                             )
                 return file_name
     
    @@ -1058,17 +1058,17 @@ 

    Source code for gen3.metadata

     
             now = str(datetime.utcnow())
             metadata = {
    -            "type": "package",
    -            "package": {
    -                "version": "0.1",
    -                "file_name": file_name,
    -                "created_time": now,
    -                "updated_time": now,
    -                "size": file_size,
    -                "hashes": hashes,
    -                "contents": contents or None,
    +            "type": "package",
    +            "package": {
    +                "version": "0.1",
    +                "file_name": file_name,
    +                "created_time": now,
    +                "updated_time": now,
    +                "size": file_size,
    +                "hashes": hashes,
    +                "contents": contents or None,
                 },
    -            "_upload_status": "uploaded",
    +            "_upload_status": "uploaded",
             }
             return metadata
    diff --git a/docs/_build/html/_modules/gen3/object.html b/docs/_build/html/_modules/gen3/object.html index 19cc6697d..ac26f96c0 100644 --- a/docs/_build/html/_modules/gen3/object.html +++ b/docs/_build/html/_modules/gen3/object.html @@ -42,7 +42,7 @@

    Source code for gen3.object

     
    [docs] class Gen3Object: - """For interacting with Gen3 object level features. + """For interacting with Gen3 object level features. A class for interacting with the Gen3 object services. Currently allows creating and deleting of an object from the Gen3 System. @@ -54,36 +54,36 @@

    Source code for gen3.object

             This generates the Gen3Object class pointed at the sandbox commons while
             using the credentials.json downloaded from the commons profile page.
     
    -        >>> auth = Gen3Auth(refresh_file="credentials.json")
    +        >>> auth = Gen3Auth(refresh_file="credentials.json")
             ... object = Gen3Object(auth)
     
    -    """
    +    """
     
         def __init__(self, auth_provider=None):
             self._auth_provider = auth_provider
    -        self.service_endpoint = "/mds"
    +        self.service_endpoint = "/mds"
     
         def create_object(self, file_name, authz, metadata=None, aliases=None):
             url = (
    -            self._auth_provider.endpoint.rstrip("/")
    +            self._auth_provider.endpoint.rstrip("/")
                 + self.service_endpoint
    -            + "/objects"
    +            + "/objects"
             )
             body = {
    -            "file_name": file_name,
    -            "authz": authz,
    -            "metadata": metadata,
    -            "aliases": aliases,
    +            "file_name": file_name,
    +            "authz": authz,
    +            "metadata": metadata,
    +            "aliases": aliases,
             }
             response = requests.post(url, json=body, auth=self._auth_provider)
             raise_for_status_and_print_error(response)
             data = response.json()
    -        return data["guid"], data["upload_url"]
    +        return data["guid"], data["upload_url"]
     
     
    [docs] def delete_object(self, guid, delete_file_locations=False): - """ + """ Delete the object from indexd, metadata service and optionally all storage locations Args: @@ -91,12 +91,12 @@

    Source code for gen3.object

                 `delete_file_locations` -- if True, removes the object from existing bucket location(s) through fence
             Returns:
                 Nothing
    -        """
    -        delete_param = "?delete_file_locations" if delete_file_locations else ""
    +        """
    +        delete_param = "?delete_file_locations" if delete_file_locations else ""
             url = (
    -            self._auth_provider.endpoint.rstrip("/")
    +            self._auth_provider.endpoint.rstrip("/")
                 + self.service_endpoint
    -            + "/objects/"
    +            + "/objects/"
                 + guid
                 + delete_param
             )
    diff --git a/docs/_build/html/_modules/gen3/query.html b/docs/_build/html/_modules/gen3/query.html
    index af2587093..295750ef2 100644
    --- a/docs/_build/html/_modules/gen3/query.html
    +++ b/docs/_build/html/_modules/gen3/query.html
    @@ -39,7 +39,7 @@ 

    Source code for gen3.query

     
    [docs] class Gen3Query: - """ + """ Query ElasticSearch data from a Gen3 system. Args: @@ -49,9 +49,9 @@

    Source code for gen3.query

             This generates the Gen3Query class pointed at the sandbox commons while
             using the credentials.json downloaded from the commons profile page.
     
    -        >>> auth = Gen3Auth(endpoint, refresh_file="credentials.json")
    +        >>> auth = Gen3Auth(endpoint, refresh_file="credentials.json")
             ... query = Gen3Query(auth)
    -    """
    +    """
     
         def __init__(self, auth_provider):
             self._auth_provider = auth_provider
    @@ -70,7 +70,7 @@ 

    Source code for gen3.query

             accessibility=None,
             verbose=True,
         ):
    -        """
    +        """
             Execute a query against a Data Commons.
     
             Args:
    @@ -81,23 +81,23 @@ 

    Source code for gen3.query

                 filters: (object, optional): { field: sort method } object. Will filter data with ALL fields EQUAL to the provided respective value. If more complex filters are needed, use the `filter_object` parameter instead.
                 filter_object (object, optional): Filter to apply. For syntax details, see https://github.com/uc-cdis/guppy/blob/master/doc/queries.md#filter.
                 sort_object (object, optional): { field: sort method } object.
    -            accessibility (list, optional): One of ["accessible" (default), "unaccessible", "all"]. Only valid when querying a data type in "regular" tier access mode.
    +            accessibility (list, optional): One of ["accessible" (default), "unaccessible", "all"]. Only valid when querying a data type in "regular" tier access mode.
     
             Returns:
    -            Object: {"data": {<data_type>: [<record>, <record>, ...]}}
    +            Object: {"data": {<data_type>: [<record>, <record>, ...]}}
     
             Examples:
                 >>> Gen3Query.query(
    -                data_type="subject",
    +                data_type="subject",
                     first=50,
                     fields=[
    -                    "vital_status",
    -                    "submitter_id",
    +                    "vital_status",
    +                    "submitter_id",
                     ],
    -                filters={"vital_status": "Alive"},
    -                sort_object={"submitter_id": "asc"},
    +                filters={"vital_status": "Alive"},
    +                sort_object={"submitter_id": "asc"},
                 )
    -        """
    +        """
             if not first:
                 first = 10
             if not offset:
    @@ -105,14 +105,14 @@ 

    Source code for gen3.query

             if not sort_object:
                 sort_object = {}
             if not accessibility:
    -            accessibility = "accessible"
    +            accessibility = "accessible"
             if filters and filter_object:
                 raise Exception(
    -                "Only one of `filters` and `filter_object` can be used at a time."
    +                "Only one of `filters` and `filter_object` can be used at a time."
                 )
             if filters:
                 filter_object = {
    -                "AND": [{"=": {field: val}} for field, val in filters.items()]
    +                "AND": [{"=": {field: val}} for field, val in filters.items()]
                 }
     
             if first + offset > 10000:  # ElasticSearch limitation
    @@ -126,13 +126,13 @@ 

    Source code for gen3.query

                     first=first,
                     offset=offset,
                 )
    -            return {"data": {data_type: data}}
    +            return {"data": {data_type: data}}
     
    -        # convert sort_object to graphql: [ { field_name: "sort_method" } ]
    -        sorts = [f'{{{field}: "{val}"}}' for field, val in sort_object.items()]
    -        sort_string = f'[{", ".join(sorts)}]'
    +        # convert sort_object to graphql: [ { field_name: "sort_method" } ]
    +        sorts = [f'{{{field}: "{val}"}}' for field, val in sort_object.items()]
    +        sort_string = f'[{", ".join(sorts)}]'
     
    -        query_string = f"""query($filter: JSON) {{
    +        query_string = f"""query($filter: JSON) {{
                 {data_type}(
                     first: {first},
                     offset: {offset},
    @@ -140,17 +140,17 @@ 

    Source code for gen3.query

                     accessibility: {accessibility},
                     filter: $filter
                 ) {{
    -                {" ".join(fields)}
    +                {" ".join(fields)}
                 }}
    -        }}"""
    -        variables = {"filter": filter_object}
    +        }}"""
    +        variables = {"filter": filter_object}
             return self.graphql_query(query_string=query_string, variables=variables)
    [docs] def graphql_query(self, query_string, variables=None): - """ + """ Execute a GraphQL query against a Data Commons. Args: @@ -158,29 +158,29 @@

    Source code for gen3.query

                 variables (:obj:`object`, optional): Dictionary of variables to pass with the query.
     
             Returns:
    -            Object: {"data": {<data_type>: [<record>, <record>, ...]}}
    +            Object: {"data": {<data_type>: [<record>, <record>, ...]}}
     
             Examples:
    -            >>> query_string = "{ my_index { my_field } }"
    +            >>> query_string = "{ my_index { my_field } }"
                 ... Gen3Query.graphql_query(query_string)
    -        """
    -        url = f"{self._auth_provider.endpoint}/guppy/graphql"
    +        """
    +        url = f"{self._auth_provider.endpoint}/guppy/graphql"
             response = requests.post(
                 url,
    -            json={"query": query_string, "variables": variables},
    +            json={"query": query_string, "variables": variables},
                 auth=self._auth_provider,
             )
             try:
                 raise_for_status_and_print_error(response)
             except Exception:
                 print(
    -                f"Unable to query.\nQuery: {query_string}\nVariables: {variables}\n{response.text}"
    +                f"Unable to query.\nQuery: {query_string}\nVariables: {variables}\n{response.text}"
                 )
                 raise
             try:
                 return response.json()
             except Exception:
    -            print(f"Did not receive JSON: {response.text}")
    +            print(f"Did not receive JSON: {response.text}")
                 raise
    @@ -196,7 +196,7 @@

    Source code for gen3.query

             first=None,
             offset=None,
         ):
    -        """
    +        """
             Execute a raw data download against a Data Commons.
     
             Args:
    @@ -204,7 +204,7 @@ 

    Source code for gen3.query

                 fields (list): List of fields to return.
                 filter_object (object, optional): Filter to apply. For syntax details, see https://github.com/uc-cdis/guppy/blob/master/doc/queries.md#filter.
                 sort_fields (list, optional): List of { field: sort method } objects.
    -            accessibility (list, optional): One of ["accessible" (default), "unaccessible", "all"]. Only valid when downloading from a data type in "regular" tier access mode.
    +            accessibility (list, optional): One of ["accessible" (default), "unaccessible", "all"]. Only valid when downloading from a data type in "regular" tier access mode.
                 first (int, optional): Number of rows to return (default: all rows).
                 offset (int, optional): Starting position (default: 0).
     
    @@ -213,29 +213,29 @@ 

    Source code for gen3.query

     
             Examples:
                 >>> Gen3Query.raw_data_download(
    -                    data_type="subject",
    +                    data_type="subject",
                         fields=[
    -                        "vital_status",
    -                        "submitter_id",
    -                        "project_id"
    +                        "vital_status",
    +                        "submitter_id",
    +                        "project_id"
                         ],
    -                    filter_object={"=": {"project_id": "my_program-my_project"}},
    -                    sort_fields=[{"submitter_id": "asc"}],
    -                    accessibility="accessible"
    +                    filter_object={"=": {"project_id": "my_program-my_project"}},
    +                    sort_fields=[{"submitter_id": "asc"}],
    +                    accessibility="accessible"
                     )
    -        """
    +        """
             if not accessibility:
    -            accessibility = "accessible"
    +            accessibility = "accessible"
             if not offset:
                 offset = 0
     
    -        body = {"type": data_type, "fields": fields, "accessibility": accessibility}
    +        body = {"type": data_type, "fields": fields, "accessibility": accessibility}
             if filter_object:
    -            body["filter"] = filter_object
    +            body["filter"] = filter_object
             if sort_fields:
    -            body["sort"] = sort_fields
    +            body["sort"] = sort_fields
     
    -        url = f"{self._auth_provider.endpoint}/guppy/download"
    +        url = f"{self._auth_provider.endpoint}/guppy/download"
             response = requests.post(
                 url,
                 json=body,
    @@ -244,12 +244,12 @@ 

    Source code for gen3.query

             try:
                 raise_for_status_and_print_error(response)
             except Exception:
    -            print(f"Unable to download.\nBody: {body}\n{response.text}")
    +            print(f"Unable to download.\nBody: {body}\n{response.text}")
                 raise
             try:
                 data = response.json()
             except Exception:
    -            print(f"Did not receive JSON: {response.text}")
    +            print(f"Did not receive JSON: {response.text}")
                 raise
     
             if offset:
    diff --git a/docs/_build/html/_modules/gen3/submission.html b/docs/_build/html/_modules/gen3/submission.html
    index 04223a5fb..b82fd483e 100644
    --- a/docs/_build/html/_modules/gen3/submission.html
    +++ b/docs/_build/html/_modules/gen3/submission.html
    @@ -58,7 +58,7 @@ 

    Source code for gen3.submission

     
    [docs] class Gen3Submission: - """Submit/Export/Query data from a Gen3 Submission system. + """Submit/Export/Query data from a Gen3 Submission system. A class for interacting with the Gen3 submission services. Supports submitting and exporting from Sheepdog. @@ -71,10 +71,10 @@

    Source code for gen3.submission

             This generates the Gen3Submission class pointed at the sandbox commons while
             using the credentials.json downloaded from the commons profile page.
     
    -        >>> auth = Gen3Auth(refresh_file="credentials.json")
    +        >>> auth = Gen3Auth(refresh_file="credentials.json")
             ... sub = Gen3Submission(auth)
     
    -    """
    +    """
     
         def __init__(self, endpoint=None, auth_provider=None):
             # auth_provider legacy interface required endpoint as 1st arg
    @@ -82,18 +82,18 @@ 

    Source code for gen3.submission

             self._endpoint = self._auth_provider.endpoint
     
         def __export_file(self, filename, output):
    -        """Writes an API response to a file."""
    -        with open(filename, "w") as outfile:
    +        """Writes an API response to a file."""
    +        with open(filename, "w") as outfile:
                 outfile.write(output)
    -        print("\nOutput written to file: " + filename)
    +        print("\nOutput written to file: " + filename)
     
         ### Program functions
     
     
    [docs] def get_programs(self): - """List registered programs""" - api_url = f"{self._endpoint}/api/v0/submission/" + """List registered programs""" + api_url = f"{self._endpoint}/api/v0/submission/" output = requests.get(api_url, auth=self._auth_provider) raise_for_status_and_print_error(output) return output.json()
    @@ -102,7 +102,7 @@

    Source code for gen3.submission

     
    [docs] def create_program(self, json): - """Create a program. + """Create a program. Args: json (object): The json of the program to create @@ -110,8 +110,8 @@

    Source code for gen3.submission

                 This creates a program in the sandbox commons.
     
                 >>> Gen3Submission.create_program(json)
    -        """
    -        api_url = "{}/api/v0/submission/".format(self._endpoint)
    +        """
    +        api_url = "{}/api/v0/submission/".format(self._endpoint)
             output = requests.post(api_url, auth=self._auth_provider, json=json)
             raise_for_status_and_print_error(output)
             return output.json()
    @@ -120,7 +120,7 @@

    Source code for gen3.submission

     
    [docs] def delete_program(self, program): - """Delete a program. + """Delete a program. This deletes an empty program from the commons. @@ -128,12 +128,12 @@

    Source code for gen3.submission

                 program (str): The program to delete.
     
             Examples:
    -            This deletes the "DCF" program.
    +            This deletes the "DCF" program.
     
    -            >>> Gen3Submission.delete_program("DCF")
    +            >>> Gen3Submission.delete_program("DCF")
     
    -        """
    -        api_url = "{}/api/v0/submission/{}".format(self._endpoint, program)
    +        """
    +        api_url = "{}/api/v0/submission/{}".format(self._endpoint, program)
             output = requests.delete(api_url, auth=self._auth_provider)
             raise_for_status_and_print_error(output)
             return output
    @@ -144,7 +144,7 @@

    Source code for gen3.submission

     
    [docs] def get_projects(self, program): - """List registered projects for a given program + """List registered projects for a given program Args: program: the name of the program you want the projects from @@ -152,10 +152,10 @@

    Source code for gen3.submission

             Example:
                 This lists all the projects under the DCF program
     
    -            >>> Gen3Submission.get_projects("DCF")
    +            >>> Gen3Submission.get_projects("DCF")
     
    -        """
    -        api_url = f"{self._endpoint}/api/v0/submission/{program}"
    +        """
    +        api_url = f"{self._endpoint}/api/v0/submission/{program}"
             output = requests.get(api_url, auth=self._auth_provider)
             raise_for_status_and_print_error(output)
             return output.json()
    @@ -164,7 +164,7 @@

    Source code for gen3.submission

     
    [docs] def create_project(self, program, json): - """Create a project. + """Create a project. Args: program (str): The program to create a project on json (object): The json of the project to create @@ -172,9 +172,9 @@

    Source code for gen3.submission

             Examples:
                 This creates a project on the DCF program in the sandbox commons.
     
    -            >>> Gen3Submission.create_project("DCF", json)
    -        """
    -        api_url = "{}/api/v0/submission/{}".format(self._endpoint, program)
    +            >>> Gen3Submission.create_project("DCF", json)
    +        """
    +        api_url = "{}/api/v0/submission/{}".format(self._endpoint, program)
             output = requests.put(api_url, auth=self._auth_provider, json=json)
             raise_for_status_and_print_error(output)
             return output.json()
    @@ -183,7 +183,7 @@

    Source code for gen3.submission

     
    [docs] def delete_project(self, program, project): - """Delete a project. + """Delete a project. This deletes an empty project from the commons. @@ -192,12 +192,12 @@

    Source code for gen3.submission

                 project (str): The project to delete.
     
             Examples:
    -            This deletes the "CCLE" project from the "DCF" program.
    +            This deletes the "CCLE" project from the "DCF" program.
     
    -            >>> Gen3Submission.delete_project("DCF", "CCLE")
    +            >>> Gen3Submission.delete_project("DCF", "CCLE")
     
    -        """
    -        api_url = "{}/api/v0/submission/{}/{}".format(self._endpoint, program, project)
    +        """
    +        api_url = "{}/api/v0/submission/{}/{}".format(self._endpoint, program, project)
             output = requests.delete(api_url, auth=self._auth_provider)
             raise_for_status_and_print_error(output)
             return output
    @@ -206,7 +206,7 @@

    Source code for gen3.submission

     
    [docs] def get_project_dictionary(self, program, project): - """Get dictionary schema for a given project + """Get dictionary schema for a given project Args: program: the name of the program the project is from @@ -214,10 +214,10 @@

    Source code for gen3.submission

     
             Example:
     
    -            >>> Gen3Submission.get_project_dictionary("DCF", "CCLE")
    +            >>> Gen3Submission.get_project_dictionary("DCF", "CCLE")
     
    -        """
    -        api_url = f"{self._endpoint}/api/v0/submission/{program}/{project}/_dictionary"
    +        """
    +        api_url = f"{self._endpoint}/api/v0/submission/{program}/{project}/_dictionary"
             output = requests.get(api_url, auth=self._auth_provider)
             raise_for_status_and_print_error(output)
             return output.json()
    @@ -226,18 +226,18 @@

    Source code for gen3.submission

     
    [docs] def open_project(self, program, project): - """Mark a project ``open``. Opening a project means uploads, deletions, etc. are allowed. + """Mark a project ``open``. Opening a project means uploads, deletions, etc. are allowed. Args: program: the name of the program the project is from - project: the name of the project you want to 'open' + project: the name of the project you want to 'open' Example: - >>> Gen3Submission.get_project_manifest("DCF", "CCLE") + >>> Gen3Submission.get_project_manifest("DCF", "CCLE") - """ - api_url = f"{self._endpoint}/api/v0/submission/{program}/{project}/open" + """ + api_url = f"{self._endpoint}/api/v0/submission/{program}/{project}/open" output = requests.put(api_url, auth=self._auth_provider) raise_for_status_and_print_error(output) return output.json()
    @@ -248,7 +248,7 @@

    Source code for gen3.submission

     
    [docs] def submit_record(self, program, project, json): - """Submit record(s) to a project as json. + """Submit record(s) to a project as json. Args: program (str): The program to submit to. @@ -258,11 +258,11 @@

    Source code for gen3.submission

             Examples:
                 This submits records to the CCLE project in the sandbox commons.
     
    -            >>> Gen3Submission.submit_record("DCF", "CCLE", json)
    +            >>> Gen3Submission.submit_record("DCF", "CCLE", json)
     
    -        """
    -        api_url = "{}/api/v0/submission/{}/{}".format(self._endpoint, program, project)
    -        logging.debug("Using the Sheepdog API URL: {}".format(api_url))
    +        """
    +        api_url = "{}/api/v0/submission/{}/{}".format(self._endpoint, program, project)
    +        logging.debug("Using the Sheepdog API URL: {}".format(api_url))
     
             output = requests.put(api_url, auth=self._auth_provider, json=json)
             raise_for_status_and_print_error(output)
    @@ -272,7 +272,7 @@ 

    Source code for gen3.submission

     
    [docs] def delete_record(self, program, project, uuid): - """ + """ Delete a record from a project. Args: @@ -283,15 +283,15 @@

    Source code for gen3.submission

             Examples:
                 This deletes a record from the CCLE project in the sandbox commons.
     
    -            >>> Gen3Submission.delete_record("DCF", "CCLE", uuid)
    -        """
    +            >>> Gen3Submission.delete_record("DCF", "CCLE", uuid)
    +        """
             return self.delete_records(program, project, [uuid])
    [docs] def delete_records(self, program, project, uuids, batch_size=100): - """ + """ Delete a list of records from a project. Args: @@ -303,11 +303,11 @@

    Source code for gen3.submission

             Examples:
                 This deletes a list of records from the CCLE project in the sandbox commons.
     
    -            >>> Gen3Submission.delete_records("DCF", "CCLE", ["uuid1", "uuid2"])
    -        """
    +            >>> Gen3Submission.delete_records("DCF", "CCLE", ["uuid1", "uuid2"])
    +        """
             if not uuids:
                 return
    -        api_url = "{}/api/v0/submission/{}/{}/entities".format(
    +        api_url = "{}/api/v0/submission/{}/{}/entities".format(
                 self._endpoint, program, project
             )
             for i in itertools.count():
    @@ -315,14 +315,14 @@ 

    Source code for gen3.submission

                 if len(uuids_to_delete) == 0:
                     break
                 output = requests.delete(
    -                "{}/{}".format(api_url, ",".join(uuids_to_delete)),
    +                "{}/{}".format(api_url, ",".join(uuids_to_delete)),
                     auth=self._auth_provider,
                 )
                 try:
                     raise_for_status_and_print_error(output)
                 except requests.exceptions.HTTPError:
                     print(
    -                    "\n{}\nFailed to delete uuids: {}".format(
    +                    "\n{}\nFailed to delete uuids: {}".format(
                             output.text, uuids_to_delete
                         )
                     )
    @@ -333,7 +333,7 @@ 

    Source code for gen3.submission

     
    [docs] def delete_node(self, program, project, node_name, batch_size=100, verbose=True): - """ + """ Delete all records for a node from a project. Args: @@ -346,8 +346,8 @@

    Source code for gen3.submission

             Examples:
                 This deletes a node from the CCLE project in the sandbox commons.
     
    -            >>> Gen3Submission.delete_node("DCF", "CCLE", "demographic")
    -        """
    +            >>> Gen3Submission.delete_node("DCF", "CCLE", "demographic")
    +        """
             return self.delete_nodes(
                 program, project, [node_name], batch_size, verbose=verbose
             )
    @@ -358,7 +358,7 @@

    Source code for gen3.submission

         def delete_nodes(
             self, program, project, ordered_node_list, batch_size=100, verbose=True
         ):
    -        """
    +        """
             Delete all records for a list of nodes from a project.
     
             Args:
    @@ -371,28 +371,28 @@ 

    Source code for gen3.submission

             Examples:
                 This deletes a list of nodes from the CCLE project in the sandbox commons.
     
    -            >>> Gen3Submission.delete_nodes("DCF", "CCLE", ["demographic", "subject", "experiment"])
    -        """
    -        project_id = f"{program}-{project}"
    +            >>> Gen3Submission.delete_nodes("DCF", "CCLE", ["demographic", "subject", "experiment"])
    +        """
    +        project_id = f"{program}-{project}"
             for node in ordered_node_list:
                 if verbose:
    -                print(node, end="", flush=True)
    -            first_uuid = ""
    +                print(node, end="", flush=True)
    +            first_uuid = ""
                 while True:
    -                query_string = f"""{{
    -                    {node} (first: {batch_size}, project_id: "{project_id}") {{
    +                query_string = f"""{{
    +                    {node} (first: {batch_size}, project_id: "{project_id}") {{
                             id
                         }}
    -                }}"""
    +                }}"""
                     res = self.query(query_string)
    -                uuids = [x["id"] for x in res["data"][node]]
    +                uuids = [x["id"] for x in res["data"][node]]
                     if len(uuids) == 0:
                         break  # all done
                     if first_uuid == uuids[0]:
    -                    raise Exception("Failed to delete. Exiting")
    +                    raise Exception("Failed to delete. Exiting")
                     first_uuid = uuids[0]
                     if verbose:
    -                    print(".", end="", flush=True)
    +                    print(".", end="", flush=True)
                     self.delete_records(program, project, uuids, batch_size)
                 if verbose:
                     print()
    @@ -401,35 +401,35 @@

    Source code for gen3.submission

     
    [docs] def export_record(self, program, project, uuid, fileformat, filename=None): - """Export a single record into json. + """Export a single record into json. Args: program (str): The program the record is under. project (str): The project the record is under. uuid (str): The UUID of the record to export. - fileformat (str): Export data as either 'json' or 'tsv' + fileformat (str): Export data as either 'json' or 'tsv' filename (str): Name of the file to export to; if no filename is provided, prints data to screen Examples: This exports a single record from the sandbox commons. - >>> Gen3Submission.export_record("DCF", "CCLE", "d70b41b9-6f90-4714-8420-e043ab8b77b9", "json", filename="DCF-CCLE_one_record.json") + >>> Gen3Submission.export_record("DCF", "CCLE", "d70b41b9-6f90-4714-8420-e043ab8b77b9", "json", filename="DCF-CCLE_one_record.json") - """ + """ assert fileformat in [ - "json", - "tsv", - ], "File format must be either 'json' or 'tsv'" - api_url = "{}/api/v0/submission/{}/{}/export?ids={}&format={}".format( + "json", + "tsv", + ], "File format must be either 'json' or 'tsv'" + api_url = "{}/api/v0/submission/{}/{}/export?ids={}&format={}".format( self._endpoint, program, project, uuid, fileformat ) output = requests.get(api_url, auth=self._auth_provider).text if filename is None: - if fileformat == "json": + if fileformat == "json": try: output = json.loads(output) except ValueError as e: - print(f"Output: {output}\nUnable to parse JSON: {e}") + print(f"Output: {output}\nUnable to parse JSON: {e}") raise return output else: @@ -440,35 +440,35 @@

    Source code for gen3.submission

     
    [docs] def export_node(self, program, project, node_type, fileformat, filename=None): - """Export all records in a single node type of a project. + """Export all records in a single node type of a project. Args: program (str): The program to which records belong. project (str): The project to which records belong. node_type (str): The name of the node to export. - fileformat (str): Export data as either 'json' or 'tsv' + fileformat (str): Export data as either 'json' or 'tsv' filename (str): Name of the file to export to; if no filename is provided, prints data to screen Examples: - This exports all records in the "sample" node from the CCLE project in the sandbox commons. + This exports all records in the "sample" node from the CCLE project in the sandbox commons. - >>> Gen3Submission.export_node("DCF", "CCLE", "sample", "tsv", filename="DCF-CCLE_sample_node.tsv") + >>> Gen3Submission.export_node("DCF", "CCLE", "sample", "tsv", filename="DCF-CCLE_sample_node.tsv") - """ + """ assert fileformat in [ - "json", - "tsv", - ], "File format must be either 'json' or 'tsv'" - api_url = "{}/api/v0/submission/{}/{}/export/?node_label={}&format={}".format( + "json", + "tsv", + ], "File format must be either 'json' or 'tsv'" + api_url = "{}/api/v0/submission/{}/{}/export/?node_label={}&format={}".format( self._endpoint, program, project, node_type, fileformat ) output = requests.get(api_url, auth=self._auth_provider).text if filename is None: - if fileformat == "json": + if fileformat == "json": try: output = json.loads(output) except ValueError as e: - print(f"Output: {output}\nUnable to parse JSON: {e}") + print(f"Output: {output}\nUnable to parse JSON: {e}") raise return output else: @@ -481,7 +481,7 @@

    Source code for gen3.submission

     
    [docs] def query(self, query_txt, variables=None, max_tries=1): - """Execute a GraphQL query against a Data Commons. + """Execute a GraphQL query against a Data Commons. Args: query_txt (str): Query text. @@ -492,25 +492,25 @@

    Source code for gen3.submission

                 This executes a query to get the list of all the project codes for all the projects
                 in the Data Commons.
     
    -            >>> query = "{ project(first:0) { code } }"
    +            >>> query = "{ project(first:0) { code } }"
                 ... Gen3Submission.query(query)
     
    -        """
    -        api_url = "{}/api/v0/submission/graphql".format(self._endpoint)
    +        """
    +        api_url = "{}/api/v0/submission/graphql".format(self._endpoint)
             if variables == None:
    -            query = {"query": query_txt}
    +            query = {"query": query_txt}
             else:
    -            query = {"query": query_txt, "variables": variables}
    +            query = {"query": query_txt, "variables": variables}
     
             tries = 0
             while tries < max_tries:
                 output = requests.post(api_url, auth=self._auth_provider, json=query).text
                 data = json.loads(output)
     
    -            if "errors" in data:
    -                raise Gen3SubmissionQueryError(data["errors"])
    +            if "errors" in data:
    +                raise Gen3SubmissionQueryError(data["errors"])
     
    -            if not "data" in data:
    +            if not "data" in data:
                     print(query_txt)
                     print(data)
     
    @@ -522,7 +522,7 @@ 

    Source code for gen3.submission

     
    [docs] def get_graphql_schema(self): - """Returns the GraphQL schema for a commons. + """Returns the GraphQL schema for a commons. This runs the GraphQL introspection query against a commons and returns the results. @@ -531,8 +531,8 @@

    Source code for gen3.submission

     
                 >>> Gen3Submission.get_graphql_schema()
     
    -        """
    -        api_url = "{}/api/v0/submission/getschema".format(self._endpoint)
    +        """
    +        api_url = "{}/api/v0/submission/getschema".format(self._endpoint)
             output = requests.get(api_url).text
             data = json.loads(output)
             return data
    @@ -543,7 +543,7 @@

    Source code for gen3.submission

     
    [docs] def get_dictionary_node(self, node_type): - """Returns the dictionary schema for a specific node. + """Returns the dictionary schema for a specific node. This gets the current json dictionary schema for a specific node type in a commons. @@ -551,12 +551,12 @@

    Source code for gen3.submission

                 node_type (str): The node_type (or name of the node) to retrieve.
     
             Examples:
    -            This returns the dictionary schema the "subject" node.
    +            This returns the dictionary schema the "subject" node.
     
    -            >>> Gen3Submission.get_dictionary_node("subject")
    +            >>> Gen3Submission.get_dictionary_node("subject")
     
    -        """
    -        api_url = "{}/api/v0/submission/_dictionary/{}".format(
    +        """
    +        api_url = "{}/api/v0/submission/_dictionary/{}".format(
                 self._endpoint, node_type
             )
             output = requests.get(api_url).text
    @@ -567,7 +567,7 @@ 

    Source code for gen3.submission

     
    [docs] def get_dictionary_all(self): - """Returns the entire dictionary object for a commons. + """Returns the entire dictionary object for a commons. This gets a json of the current dictionary schema for a commons. @@ -576,8 +576,8 @@

    Source code for gen3.submission

     
                 >>> Gen3Submission.get_dictionary_all()
     
    -        """
    -        return self.get_dictionary_node("_all")
    + """ + return self.get_dictionary_node("_all")
    ### File functions @@ -585,7 +585,7 @@

    Source code for gen3.submission

     
    [docs] def get_project_manifest(self, program, project): - """Get a projects file manifest + """Get a projects file manifest Args: program: the name of the program the project is from @@ -593,10 +593,10 @@

    Source code for gen3.submission

     
             Example:
     
    -            >>> Gen3Submission.get_project_manifest("DCF", "CCLE")
    +            >>> Gen3Submission.get_project_manifest("DCF", "CCLE")
     
    -        """
    -        api_url = f"{self._endpoint}/api/v0/submission/{program}/{project}/manifest"
    +        """
    +        api_url = f"{self._endpoint}/api/v0/submission/{program}/{project}/manifest"
             output = requests.get(api_url, auth=self._auth_provider)
             return output
    @@ -604,51 +604,51 @@

    Source code for gen3.submission

     
    [docs] def submit_file(self, project_id, filename, chunk_size=30, row_offset=0): - """Submit data in a spreadsheet file containing multiple records in rows to a Gen3 Data Commons. + """Submit data in a spreadsheet file containing multiple records in rows to a Gen3 Data Commons. Args: project_id (str): The project_id to submit to. filename (str): The file containing data to submit. The format can be TSV, CSV or XLSX (first worksheet only for now). chunk_size (integer): The number of rows of data to submit for each request to the API. - row_offset (integer): The number of rows of data to skip; '0' starts submission from the first row and submits all data. + row_offset (integer): The number of rows of data to skip; '0' starts submission from the first row and submits all data. Examples: This submits a spreadsheet file containing multiple records in rows to the CCLE project in the sandbox commons. - >>> Gen3Submission.submit_file("DCF-CCLE","data_spreadsheet.tsv") + >>> Gen3Submission.submit_file("DCF-CCLE","data_spreadsheet.tsv") - """ + """ # Read the file in as a pandas DataFrame f = os.path.basename(filename) - if f.lower().endswith(".csv"): - df = pd.read_csv(filename, header=0, sep=",", dtype=str).fillna("") - elif f.lower().endswith(".xlsx"): + if f.lower().endswith(".csv"): + df = pd.read_csv(filename, header=0, sep=",", dtype=str).fillna("") + elif f.lower().endswith(".xlsx"): xl = pd.ExcelFile(filename) # load excel file sheet = xl.sheet_names[0] # sheetname df = xl.parse(sheet) # save sheet as dataframe converters = { col: str for col in list(df) - } # make sure int isn't converted to float - df = pd.read_excel(filename, converters=converters).fillna("") # remove nan - elif filename.lower().endswith((".tsv", ".txt")): - df = pd.read_csv(filename, header=0, sep="\t", dtype=str).fillna("") + } # make sure int isn't converted to float + df = pd.read_excel(filename, converters=converters).fillna("") # remove nan + elif filename.lower().endswith((".tsv", ".txt")): + df = pd.read_csv(filename, header=0, sep="\t", dtype=str).fillna("") else: - raise Gen3UserError("Please upload a file in CSV, TSV, or XLSX format.") + raise Gen3UserError("Please upload a file in CSV, TSV, or XLSX format.") df.rename( - columns={c: c.lstrip("*") for c in df.columns}, inplace=True + columns={c: c.lstrip("*") for c in df.columns}, inplace=True ) # remove any leading asterisks in the DataFrame column names # Check uniqueness of submitter_ids: if len(list(df.submitter_id)) != len(list(df.submitter_id.unique())): raise Gen3Error( - "Warning: file contains duplicate submitter_ids. \nNote: submitter_ids must be unique within a node!" + "Warning: file contains duplicate submitter_ids. \nNote: submitter_ids must be unique within a node!" ) # Chunk the file - print("\nSubmitting {} with {} records.".format(filename, len(df))) - program, project = project_id.split("-", 1) - api_url = "{}/api/v0/submission/{}/{}".format(self._endpoint, program, project) - headers = {"content-type": "text/tab-separated-values"} + print("\nSubmitting {} with {} records.".format(filename, len(df))) + program, project = project_id.split("-", 1) + api_url = "{}/api/v0/submission/{}/{}".format(self._endpoint, program, project) + headers = {"content-type": "text/tab-separated-values"} start = row_offset end = row_offset + chunk_size @@ -657,11 +657,11 @@

    Source code for gen3.submission

             count = 0
     
             results = {
    -            "invalid": {},  # these are invalid records
    -            "other": [],  # any unhandled API responses
    -            "details": [],  # entire API response details
    -            "succeeded": [],  # list of submitter_ids that were successfully updated/created
    -            "responses": [],  # list of API response codes
    +            "invalid": {},  # these are invalid records
    +            "other": [],  # any unhandled API responses
    +            "details": [],  # entire API response details
    +            "succeeded": [],  # list of submitter_ids that were successfully updated/created
    +            "responses": [],  # list of API response codes
             }
     
             # Start the chunking loop:
    @@ -671,10 +671,10 @@ 

    Source code for gen3.submission

                 invalid = []
                 count += 1
                 print(
    -                "Chunk {} (chunk size: {}, submitted: {} of {})".format(
    +                "Chunk {} (chunk size: {}, submitted: {} of {})".format(
                         count,
                         chunk_size,
    -                    len(results["succeeded"]) + len(results["invalid"]),
    +                    len(results["succeeded"]) + len(results["invalid"]),
                         len(df),
                     )
                 )
    @@ -683,22 +683,22 @@ 

    Source code for gen3.submission

                     response = requests.put(
                         api_url,
                         auth=self._auth_provider,
    -                    data=chunk.to_csv(sep="\t", index=False),
    +                    data=chunk.to_csv(sep="\t", index=False),
                         headers=headers,
                     ).text
                 except requests.exceptions.ConnectionError as e:
    -                results["details"].append(e.message)
    +                results["details"].append(e.message)
                     continue
     
                 # Handle the API response
                 if (
    -                "Request Timeout" in response
    -                or "413 Request Entity Too Large" in response
    -                or "Connection aborted." in response
    -                or "service failure - try again later" in response
    +                "Request Timeout" in response
    +                or "413 Request Entity Too Large" in response
    +                or "Connection aborted." in response
    +                or "service failure - try again later" in response
                 ):  # time-out, response is not valid JSON at the moment
    -                print("\t Reducing Chunk Size: {}".format(response))
    -                results["responses"].append("Reducing Chunk Size: {}".format(response))
    +                print("\t Reducing Chunk Size: {}".format(response))
    +                results["responses"].append("Reducing Chunk Size: {}".format(response))
                     timeout = True
     
                 else:
    @@ -707,72 +707,72 @@ 

    Source code for gen3.submission

                     except ValueError as e:
                         print(response)
                         print(str(e))
    -                    raise Gen3Error("Unable to parse API response as JSON!")
    +                    raise Gen3Error("Unable to parse API response as JSON!")
     
    -                if "message" in json_res and "code" not in json_res:
    +                if "message" in json_res and "code" not in json_res:
                         print(
    -                        "\t No code in the API response for Chunk {}: {}".format(
    -                            count, json_res.get("message")
    +                        "\t No code in the API response for Chunk {}: {}".format(
    +                            count, json_res.get("message")
                             )
                         )
    -                    print("\t {}".format(json_res.get("transactional_errors")))
    -                    results["responses"].append(
    -                        "Error Chunk {}: {}".format(count, json_res.get("message"))
    +                    print("\t {}".format(json_res.get("transactional_errors")))
    +                    results["responses"].append(
    +                        "Error Chunk {}: {}".format(count, json_res.get("message"))
                         )
    -                    results["other"].append(json_res.get("transactional_errors"))
    +                    results["other"].append(json_res.get("transactional_errors"))
     
    -                elif "code" not in json_res:
    -                    print("\t Unhandled API-response: {}".format(response))
    -                    results["responses"].append(
    -                        "Unhandled API response: {}".format(response)
    +                elif "code" not in json_res:
    +                    print("\t Unhandled API-response: {}".format(response))
    +                    results["responses"].append(
    +                        "Unhandled API response: {}".format(response)
                         )
     
    -                elif json_res["code"] == 200:  # success
    -                    entities = json_res.get("entities", [])
    -                    print("\t Succeeded: {} entities.".format(len(entities)))
    -                    results["responses"].append(
    -                        "Chunk {} Succeeded: {} entities.".format(count, len(entities))
    +                elif json_res["code"] == 200:  # success
    +                    entities = json_res.get("entities", [])
    +                    print("\t Succeeded: {} entities.".format(len(entities)))
    +                    results["responses"].append(
    +                        "Chunk {} Succeeded: {} entities.".format(count, len(entities))
                         )
     
                         for entity in entities:
    -                        sid = entity["unique_keys"][0]["submitter_id"]
    -                        results["succeeded"].append(sid)
    +                        sid = entity["unique_keys"][0]["submitter_id"]
    +                        results["succeeded"].append(sid)
     
    -                elif json_res["code"] == 500:  # internal server error
    -                    print("\t Internal Server Error: {}".format(response))
    -                    results["responses"].append(
    -                        "Internal Server Error: {}".format(response)
    +                elif json_res["code"] == 500:  # internal server error
    +                    print("\t Internal Server Error: {}".format(response))
    +                    results["responses"].append(
    +                        "Internal Server Error: {}".format(response)
                         )
     
                     else:  # failure (400, 401, 403, 404...)
    -                    entities = json_res.get("entities", [])
    +                    entities = json_res.get("entities", [])
                         print(
    -                        "\tChunk Failed (status code {}): {} entities.".format(
    -                            json_res.get("code"), len(entities)
    +                        "\tChunk Failed (status code {}): {} entities.".format(
    +                            json_res.get("code"), len(entities)
                             )
                         )
    -                    results["responses"].append(
    -                        "Chunk {} Failed: {} entities.".format(count, len(entities))
    +                    results["responses"].append(
    +                        "Chunk {} Failed: {} entities.".format(count, len(entities))
                         )
     
                         for entity in entities:
    -                        sid = entity["unique_keys"][0]["submitter_id"]
    -                        if entity["valid"]:  # valid but failed
    +                        sid = entity["unique_keys"][0]["submitter_id"]
    +                        if entity["valid"]:  # valid but failed
                                 valid_but_failed.append(sid)
                             else:  # invalid and failed
    -                            message = str(entity["errors"])
    -                            results["invalid"][sid] = message
    +                            message = str(entity["errors"])
    +                            results["invalid"][sid] = message
                                 invalid.append(sid)
    -                    print("\tInvalid records in this chunk: {}".format(len(invalid)))
    +                    print("\tInvalid records in this chunk: {}".format(len(invalid)))
     
                 if (
                     len(valid_but_failed) > 0 and len(invalid) > 0
                 ):  # if valid entities failed bc grouped with invalid, retry submission
                     chunk = chunk.loc[
    -                    df["submitter_id"].isin(valid_but_failed)
    -                ]  # these are records that weren't successful because they were part of a chunk that failed, but are valid and can be resubmitted without changes
    +                    df["submitter_id"].isin(valid_but_failed)
    +                ]  # these are records that weren't successful because they were part of a chunk that failed, but are valid and can be resubmitted without changes
                     print(
    -                    "Retrying submission of valid entities from failed chunk: {} valid entities.".format(
    +                    "Retrying submission of valid entities from failed chunk: {} valid entities.".format(
                             len(chunk)
                         )
                     )
    @@ -781,10 +781,10 @@ 

    Source code for gen3.submission

                     len(valid_but_failed) > 0 and len(invalid) == 0
                 ):  # if all entities are valid but submission still failed, probably due to duplicate submitter_ids. Can remove this section once the API response is fixed: https://ctds-planx.atlassian.net/browse/PXP-3065
                     raise Gen3Error(
    -                    "Please check your data for correct file encoding, special characters, or duplicate submitter_ids or ids."
    +                    "Please check your data for correct file encoding, special characters, or duplicate submitter_ids or ids."
                     )
     
    -            elif timeout is False:  # get new chunk if didn't timeout
    +            elif timeout is False:  # get new chunk if didn't timeout
                     start += chunk_size
                     end = start + chunk_size
                     chunk = df[start:end]
    @@ -795,18 +795,18 @@ 

    Source code for gen3.submission

                         end = start + chunk_size
                         chunk = df[start:end]
                         print(
    -                        "Retrying Chunk with reduced chunk_size: {}".format(chunk_size)
    +                        "Retrying Chunk with reduced chunk_size: {}".format(chunk_size)
                         )
                         timeout = False
                     else:
    -                    print("Last chunk:\n{}".format(chunk))
    +                    print("Last chunk:\n{}".format(chunk))
                         raise Gen3Error(
    -                        "Submission is timing out. Please contact the Helpdesk."
    +                        "Submission is timing out. Please contact the Helpdesk."
                         )
     
    -        print("Finished data submission.")
    -        print("Successful records: {}".format(len(set(results["succeeded"]))))
    -        print("Failed invalid records: {}".format(len(results["invalid"])))
    +        print("Finished data submission.")
    +        print("Successful records: {}".format(len(set(results["succeeded"]))))
    +        print("Failed invalid records: {}".format(len(results["invalid"])))
     
             return results
    diff --git a/docs/_build/html/_modules/gen3/tools/download/drs_download.html b/docs/_build/html/_modules/gen3/tools/download/drs_download.html index aa0e4de32..29009144c 100644 --- a/docs/_build/html/_modules/gen3/tools/download/drs_download.html +++ b/docs/_build/html/_modules/gen3/tools/download/drs_download.html @@ -31,7 +31,7 @@

    Source code for gen3.tools.download.drs_download

    -"""
    +"""
     Module for downloading and listing JSON DRS manifest and DRS objects. The main classes in
     this module for downloading DRS objects are DownloadManager and Manifest.
     
    @@ -39,16 +39,16 @@ 

    Source code for gen3.tools.download.drs_download

    This generates the Gen3Jobs class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page. - >>> datafiles = Manifest.load('sample/manifest_1.json') - downloadManager = DownloadManager("source.my_commons.org", - Gen3Auth(refresh_file="~.gen3/my_credentials.json"), datafiles) + >>> datafiles = Manifest.load('sample/manifest_1.json') + downloadManager = DownloadManager("source.my_commons.org", + Gen3Auth(refresh_file="~.gen3/my_credentials.json"), datafiles) for i in datafiles: print(i) - downloadManager.download(datafiles, ".") + downloadManager.download(datafiles, ".") See docs/howto/drsDownloading.md for more details -""" +""" import re @@ -78,7 +78,7 @@

    Source code for gen3.tools.download.drs_download

    DEFAULT_EXPIRE: timedelta = timedelta(hours=1) # package formats we handle for unpacking -PACKAGE_EXTENSIONS = [".zip"] +PACKAGE_EXTENSIONS = [".zip"] logger = get_logger(__name__) @@ -89,7 +89,7 @@

    Source code for gen3.tools.download.drs_download

    @dataclass_json(letter_case=LetterCase.SNAKE, undefined=Undefined.EXCLUDE) @dataclass class Manifest: - """Data class representing a Gen3 JSON manifest typically exported from a Gen3 discovery page. + """Data class representing a Gen3 JSON manifest typically exported from a Gen3 discovery page. The class is passed to the DownloadManager to download or list all of the files in the manifest. The Download manager will cache additional information (if available) @@ -100,7 +100,7 @@

    Source code for gen3.tools.download.drs_download

    file_name(Optional[str]): the name of the file pointed to by the DRS object id md5sum(Optional[str]): the checksum of the object commons_url(Optional[str]): url of the indexd server to retrieve file/bundle from - """ + """ object_id: str # only required member file_size: Optional[int] = -1 # -1 indicated not set @@ -112,8 +112,8 @@

    Source code for gen3.tools.download.drs_download

    [docs] @staticmethod def load_manifest(path: Path): - """Loads a json manifest""" - with open(path, "rt") as fin: + """Loads a json manifest""" + with open(path, "rt") as fin: data = json_load(fin) return Manifest.schema().load(data, many=True)
    @@ -121,14 +121,14 @@

    Source code for gen3.tools.download.drs_download

    [docs] @staticmethod - def create_object_list(manifest) -> List["Downloadable"]: - """Create a list of Downloadable instances from the manifest + def create_object_list(manifest) -> List["Downloadable"]: + """Create a list of Downloadable instances from the manifest Args: manifest(list): list of manifest objects Returns: List of Downloadable instances - """ + """ results = [] for entry in manifest: results.append( @@ -145,8 +145,8 @@

    Source code for gen3.tools.download.drs_download

    [docs] @staticmethod - def load(filename: Path) -> Optional[List["Downloadable"]]: - """ + def load(filename: Path) -> Optional[List["Downloadable"]]: + """ Method to load a json manifest and return a list of Bownloadable object. This list is passed to the DownloadManager methods of download, and list @@ -154,14 +154,14 @@

    Source code for gen3.tools.download.drs_download

    filename(Path): path to manifest file Returns: list of Downloadable objects if successfully opened/parsed None otherwise - """ + """ try: manifest = Manifest.load_manifest(filename) return Manifest.create_object_list(manifest) except FileNotFoundError as ex: - logger.critical(f"Error file not found: {ex.filename}") + logger.critical(f"Error file not found: {ex.filename}") except JSONDecodeError as ex: - logger.critical(f"format of manifest file is valid JSON: {ex.msg}") + logger.critical(f"format of manifest file is valid JSON: {ex.msg}") return None
    @@ -170,7 +170,7 @@

    Source code for gen3.tools.download.drs_download

    @dataclass class KnownDRSEndpoint: - """ + """ Dataclass used internally by the DownloadManager class to cache hostnames and tokens for Gen3 commons possibly accessed using the Workspace Token Service (WTS). The endpoint is assumed to support DRS and therefore caches additional DRS information. @@ -183,7 +183,7 @@

    Source code for gen3.tools.download.drs_download

    identifier (Optional[str]): DRS prefix (if used) use_wts (bool): if True use WTS to create tokens - """ + """ hostname: str expire: datetime = None @@ -194,27 +194,27 @@

    Source code for gen3.tools.download.drs_download

    @property def available(self): - """If endpoint has access token it is available""" + """If endpoint has access token it is available""" return self.access_token is not None def expired(self) -> bool: - """check if WTS token is older than the default expiration date + """check if WTS token is older than the default expiration date If not using the WTS return false and use standard Gen3 Auth - """ + """ if not self.use_wts: return False return datetime.now() > self.expire def renew_token(self, wts_server_name: str, server_access_token): - """Gets a new token from the WTS and updates the token and refresh time + """Gets a new token from the WTS and updates the token and refresh time Args: wts_server_name (str): hostname of WTS server server_access_token (str): token used to authenticate use of WTS - """ + """ token = wts_get_token( hostname=wts_server_name, idp=self.idp, @@ -224,22 +224,22 @@

    Source code for gen3.tools.download.drs_download

    # TODO: this would break if user is trying to download object from different commons # keep BRH token and wts sparate self.access_token = token - self.expire = datetime.fromtimestamp(token_info["exp"]) + self.expire = datetime.fromtimestamp(token_info["exp"]) class DRSObjectType(str, Enum): - """Enum defining the 3 possible DRS object types.""" + """Enum defining the 3 possible DRS object types.""" - unknown = "unknown" - object = "object" - bundle = "bundle" + unknown = "unknown" + object = "object" + bundle = "bundle"
    [docs] @dataclass class Downloadable: - """ + """ Class handling the information for a DRS object. The information is populated from the manifest or by retrieving the information from a DRS server. @@ -254,7 +254,7 @@

    Source code for gen3.tools.download.drs_download

    access_methods (List[Dict[str, Any]]): list of access methods (e.g. s3) for DRS object children (List[Downloadable]): list of child objects (in the case of DRS bundles) _manager (DownloadManager): manager for this Downloadable - """ + """ object_id: str object_type: Optional[DRSObjectType] = DRSObjectType.unknown @@ -264,52 +264,52 @@

    Source code for gen3.tools.download.drs_download

    updated_time: Optional[datetime] = None created_time: Optional[datetime] = None access_methods: List[Dict[str, Any]] = field(default_factory=list) - children: List["Downloadable"] = field(default_factory=list) + children: List["Downloadable"] = field(default_factory=list) _manager = None def __str__(self): return ( - f'{self.file_name if self.file_name is not None else "not available" : >45}; ' - f"{humanfriendly.format_size(self.file_size) :>12}; " - f'{self.hostname if self.hostname is not None else "not resolved"}; ' - f'{self.created_time.strftime("%m/%d/%Y, %H:%M:%S") if self.created_time is not None else "not available"}' + f'{self.file_name if self.file_name is not None else "not available" : >45}; ' + f"{humanfriendly.format_size(self.file_size) :>12}; " + f'{self.hostname if self.hostname is not None else "not resolved"}; ' + f'{self.created_time.strftime("%m/%d/%Y, %H:%M:%S") if self.created_time is not None else "not available"}' ) def __repr__(self): return ( - f'(Downloadable: {self.file_name if self.file_name is not None else "not available"}; ' - f"{humanfriendly.format_size(self.file_size)}; " - f'{self.hostname if self.hostname is not None else "not resolved"}; ' - f'{self.created_time.strftime("%m/%d/%Y, %H:%M:%S") if self.created_time is not None else "not available"})' + f'(Downloadable: {self.file_name if self.file_name is not None else "not available"}; ' + f"{humanfriendly.format_size(self.file_size)}; " + f'{self.hostname if self.hostname is not None else "not resolved"}; ' + f'{self.created_time.strftime("%m/%d/%Y, %H:%M:%S") if self.created_time is not None else "not available"})' )
    [docs] def download(self): - """calls the manager to download this object. Allows Downloadables to be self downloading""" + """calls the manager to download this object. Allows Downloadables to be self downloading""" self._manager.download([self])
    [docs] - def pprint(self, indent: str = ""): - """ + def pprint(self, indent: str = ""): + """ Pretty prints the object information. This is used for listing an object. In the case of a DRS bundle the child objects are listed similar to the linux tree command - """ + """ from os import linesep res = self.__str__() + linesep - child_indent = f"{indent} " + child_indent = f"{indent} " pos = -1 for x in self.children: pos += 1 if pos == len(self.children) - 1: - res += f"{child_indent}└── {x.pprint(child_indent)}" + res += f"{child_indent}└── {x.pprint(child_indent)}" else: - res += f"{child_indent}├── {x.pprint(child_indent)}" + res += f"{child_indent}├── {x.pprint(child_indent)}" return res
    @@ -319,31 +319,31 @@

    Source code for gen3.tools.download.drs_download

    [docs] @dataclass class DownloadStatus: - """Stores the download status of objectIDs. + """Stores the download status of objectIDs. The DataManager will return a list of DownloadStatus as a result of calling the download method - Status is "pending" until it is downloaded or an error occurs. + Status is "pending" until it is downloaded or an error occurs. Attributes: filename (str): the name of the file to download - status (str): status of file download initially "pending" + status (str): status of file download initially "pending" start_time (Optional[datetime]): start time of download as datetime initially None end_time (Optional[datetime]): end time of download as datetime initially None - """ + """ filename: str - status: str = "pending" + status: str = "pending" start_time: Optional[datetime] = None end_time: Optional[datetime] = None status_code: Optional[int] = None def __str__(self): return ( - f'filename: {self.filename if self.filename is not None else "not available"}; ' - f"status: {self.status}; " - f"status_code: {self.status_code}; " - f'start_time: {self.start_time.strftime("%m/%d/%Y, %H:%M:%S") if self.start_time is not None else "n/a"}; ' - f'end_time: {self.end_time.strftime("%m/%d/%Y, %H:%M:%S") if self.start_time is not None else "n/a"}' + f'filename: {self.filename if self.filename is not None else "not available"}; ' + f"status: {self.status}; " + f"status_code: {self.status_code}; " + f'start_time: {self.start_time.strftime("%m/%d/%Y, %H:%M:%S") if self.start_time is not None else "n/a"}; ' + f'end_time: {self.end_time.strftime("%m/%d/%Y, %H:%M:%S") if self.start_time is not None else "n/a"}' ) def __repr__(self): @@ -352,7 +352,7 @@

    Source code for gen3.tools.download.drs_download

    def wts_external_oidc(hostname: str) -> Dict[str, Any]: - """ + """ Get the external_oidc from a connected WTS. Will report if WTS service is missing. Note that in some cases this can be considered a warning not a error. @@ -361,80 +361,80 @@

    Source code for gen3.tools.download.drs_download

    Returns: dict containing the oidc information - """ + """ oidc = {} if not hostname: return oidc - url = f"https://{hostname}/wts/external_oidc/" - err_msg = "Likely no WTS service running on this Commons. Proceeding, but certain commands might fail." + url = f"https://{hostname}/wts/external_oidc/" + err_msg = "Likely no WTS service running on this Commons. Proceeding, but certain commands might fail." try: response = requests.get(url) response.raise_for_status() except requests.exceptions.HTTPError as exc: resp_msg = json_loads(exc.response.text) - if "message" in resp_msg: - resp_msg = resp_msg["message"] + if "message" in resp_msg: + resp_msg = resp_msg["message"] logger.warning( - f"HTTP Error ({exc.response.status_code}) from '{url}': {resp_msg}. {err_msg}" + f"HTTP Error ({exc.response.status_code}) from '{url}': {resp_msg}. {err_msg}" ) return oidc try: data = response.json() - if "providers" not in data: - logger.warning(f'No "providers" field in WTS response: {data}. {err_msg}') + if "providers" not in data: + logger.warning(f'No "providers" field in WTS response: {data}. {err_msg}') return oidc - for item in data["providers"]: - oidc[urlparse(item["base_url"]).netloc] = item + for item in data["providers"]: + oidc[urlparse(item["base_url"]).netloc] = item except JSONDecodeError as ex: - logger.warning(f"Unable to process WTS response: {response.text}. {err_msg}") + logger.warning(f"Unable to process WTS response: {response.text}. {err_msg}") return oidc def wts_get_token(hostname: str, idp: str, access_token: str): - """ + """ Gets a auth token from a Gen3 WTS server for the supplied idp Args: - hostname (str): Gen3 common's WTS service + hostname (str): Gen3 common's WTS service idp: identity provider to use access_token: Gen3 Auth to use to with WTS Returns: Token for idp if successful, None if failure - """ + """ headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "Connection": "keep-alive", - "Authorization": "bearer " + access_token, + "Content-Type": "application/json", + "Accept": "application/json", + "Connection": "keep-alive", + "Authorization": "bearer " + access_token, } try: - url = f"https://{hostname}/wts/token/?idp={idp}" + url = f"https://{hostname}/wts/token/?idp={idp}" try: response = requests.get(url=url, headers=headers) response.raise_for_status() except requests.exceptions.HTTPError as exc: logger.critical( - f"HTTP Error ({exc.response.status_code}): getting WTS token: {exc.response.text}" + f"HTTP Error ({exc.response.status_code}): getting WTS token: {exc.response.text}" ) logger.critical( - "Please make sure the target commons is connected on your profile page and that connection has not expired." + "Please make sure the target commons is connected on your profile page and that connection has not expired." ) return None - return _handle_access_token_response(response, "token") + return _handle_access_token_response(response, "token") except Gen3AuthError: - logger.critical(f"Unable to authenticate your credentials with {hostname}") + logger.critical(f"Unable to authenticate your credentials with {hostname}") return None def get_drs_object_info(hostname: str, object_id: str) -> Optional[dict]: - """ + """ Retrieves information for a DRS object residing on the hostname Args: hostname (str): hostname of DRS object @@ -442,9 +442,9 @@

    Source code for gen3.tools.download.drs_download

    Returns: GAG4H DRS object information if sucessful otherwise None - """ + """ try: - response = requests.get(f"https://{hostname}/ga4gh/drs/v1/objects/{object_id}") + response = requests.get(f"https://{hostname}/ga4gh/drs/v1/objects/{object_id}") response.raise_for_status() data = response.json() return data @@ -452,33 +452,33 @@

    Source code for gen3.tools.download.drs_download

    except requests.HTTPError as exc: if exc.response.status_code == 404: logger.critical( - f"HTTP Error ({exc.response.status_code}): {object_id} not found at {hostname}" + f"HTTP Error ({exc.response.status_code}): {object_id} not found at {hostname}" ) else: logger.critical( - f"HTTP Error ({exc.response.status_code}): accessing object: {object_id}" + f"HTTP Error ({exc.response.status_code}): accessing object: {object_id}" ) return None except ConnectionError as exc: - logger.critical(f"Connection Error {exc} when accessing object: {object_id}") + logger.critical(f"Connection Error {exc} when accessing object: {object_id}") return None def extract_filename_from_object_info(object_info: dict) -> Optional[str]: - """Extracts the filename from the object_info. + """Extracts the filename from the object_info. if filename is in object_info use that, otherwise try to extract it from the one of the access methods. Returns filename if found, else return None Args object_info (dict): DRS object dictionary - """ - if "name" in object_info and object_info["name"]: - return object_info["name"] + """ + if "name" in object_info and object_info["name"]: + return object_info["name"] - for access_method in object_info["access_methods"]: - url = access_method["access_url"]["url"] - parts = url.split("/") + for access_method in object_info["access_methods"]: + url = access_method["access_url"]["url"] + parts = url.split("/") if parts: return parts[-1] @@ -486,7 +486,7 @@

    Source code for gen3.tools.download.drs_download

    def get_access_methods(object_info: dict) -> List[str]: - """ + """ Returns the DRS GA4GH access methods from the object_info. Args: @@ -494,46 +494,46 @@

    Source code for gen3.tools.download.drs_download

    Returns: List of access methods - """ + """ if object_info is None: - logger.critical("no access methods defined for this file") + logger.critical("no access methods defined for this file") return [] - return object_info["access_methods"] + return object_info["access_methods"] def get_drs_object_type(object_info: dict) -> DRSObjectType: - """From the object info determine the type of object. + """From the object info determine the type of object. Args: object_info (dict): DRS object dictionary Returns: type of object: either bundle or object - """ - if "form" in object_info: - if object_info["form"] is None: + """ + if "form" in object_info: + if object_info["form"] is None: return DRSObjectType.object - return DRSObjectType(object_info["form"]) + return DRSObjectType(object_info["form"]) - if "contents" in object_info and len(object_info["contents"]) > 0: + if "contents" in object_info and len(object_info["contents"]) > 0: return DRSObjectType.bundle else: return DRSObjectType.object def get_drs_object_timestamp(s: Optional[str]) -> Optional[datetime]: - """returns the timestamp in datetime if not none otherwise returns None + """returns the timestamp in datetime if not none otherwise returns None Args: s (Optional[str]): string to parse Returns: datetime if not None - """ + """ return date_parser.parse(s) if s is not None else None def add_drs_object_info(info: Downloadable) -> bool: - """ + """ Given a downloader object fill in the required fields from the resolved hostname. In the case of a bundle, try to resolve all object_ids contained in the bundle including other objects and bundles. @@ -542,7 +542,7 @@

    Source code for gen3.tools.download.drs_download

    info (Downloadable): Downloadable to add information to Returns: True if object is valid and resolvable. - """ + """ if info.hostname is None: return False @@ -552,17 +552,17 @@

    Source code for gen3.tools.download.drs_download

    # Get common information we want info.file_name = extract_filename_from_object_info(object_info) - info.file_size = object_info.get("size", -1) - info.updated_time = get_drs_object_timestamp(object_info.get("updated_time", None)) - info.created_time = get_drs_object_timestamp(object_info.get("created_time", None)) + info.file_size = object_info.get("size", -1) + info.updated_time = get_drs_object_timestamp(object_info.get("updated_time", None)) + info.created_time = get_drs_object_timestamp(object_info.get("created_time", None)) info.object_type = get_drs_object_type(object_info) if info.object_type == DRSObjectType.object: info.access_methods = get_access_methods(object_info) return True else: # a bundle,get everything else - for item in object_info["contents"]: - child_id = item.get("id", None) + for item in object_info["contents"]: + child_id = item.get("id", None) if child_id is None: continue child_object = Downloadable(hostname=info.hostname, object_id=child_id) @@ -573,9 +573,9 @@

    Source code for gen3.tools.download.drs_download

    class InvisibleProgress: - """ + """ Invisible progress bar which stubs a tqdm progress bar - """ + """ def update(self, value): # pragma: no cover pass @@ -584,7 +584,7 @@

    Source code for gen3.tools.download.drs_download

    def download_file_from_url( url: str, filename: Path, show_progress: bool = True ) -> bool: - """ + """ Downloads a file using the URL. The URL is a pre-signed url created by the download manager from the access method of the DRS object. @@ -595,56 +595,56 @@

    Source code for gen3.tools.download.drs_download

    Returns: True if object has been downloaded - """ + """ try: response = requests.get(url, stream=True) response.raise_for_status() except requests.exceptions.Timeout: - logger.critical(f"Was unable to get the download url: {url}. Timeout Error.") + logger.critical(f"Was unable to get the download url: {url}. Timeout Error.") return False except requests.exceptions.HTTPError as exc: logger.critical( - f"HTTP Error ({exc.response.status_code}): downloading file from {url}" + f"HTTP Error ({exc.response.status_code}): downloading file from {url}" ) return False - total_size_in_bytes = int(response.headers.get("content-length", 0)) + total_size_in_bytes = int(response.headers.get("content-length", 0)) if total_size_in_bytes == 0: - logger.warning(f"content-length is 0") + logger.warning(f"content-length is 0") total_downloaded = 0 block_size = 8092 # 8K blocks might want to tune this. progress_bar = ( tqdm( - desc=f"{str(filename) : <45}", + desc=f"{str(filename) : <45}", total=total_size_in_bytes, - unit="iB", + unit="iB", unit_scale=True, - bar_format="{l_bar:45}{bar:35}{r_bar}{bar:-10b}", + bar_format="{l_bar:45}{bar:35}{r_bar}{bar:-10b}", ) if show_progress else InvisibleProgress() ) - # if the file name contains '/', create subdirectories and download there + # if the file name contains '/', create subdirectories and download there ensure_dirpath_exists(Path(os.path.dirname(filename))) try: - with open(filename, "wb") as file: + with open(filename, "wb") as file: for data in response.iter_content(block_size): progress_bar.update(len(data)) total_downloaded += len(data) file.write(data) except IOError as ex: - logger.critical(f"IOError opening {filename} for writing: {ex}") + logger.critical(f"IOError opening {filename} for writing: {ex}") return False if total_downloaded != total_size_in_bytes: logger.critical( - f"Error in downloading {filename}: expected {total_size_in_bytes} bytes, downloaded {total_downloaded} bytes" + f"Error in downloading {filename}: expected {total_size_in_bytes} bytes, downloaded {total_downloaded} bytes" ) return False return True @@ -652,16 +652,16 @@

    Source code for gen3.tools.download.drs_download

    def unpackage_object(filepath: str): # allowed formats are set in PACKAGE_EXTENSIONS - with zipfile.ZipFile(filepath, "r") as package: + with zipfile.ZipFile(filepath, "r") as package: package.extractall(os.path.dirname(filepath)) def parse_drs_identifier(drs_candidate: str) -> Tuple[str, str, str]: - """ + """ Parses a DRS identifier to extract a hostname in the case of hostname based DRS otherwise it look for a DRS compact identifier. - If neither one is recognized return an empty string and a type of 'unknown' + If neither one is recognized return an empty string and a type of 'unknown' Note: The regex expressions used to extract hostname or identifier has a potential for a false positive. @@ -669,30 +669,30 @@

    Source code for gen3.tools.download.drs_download

    drs_candidate (str): a drs object identifier Returns: - Tuple (str): tuple of hostname/drs prefix, guid, string: one of "hostname", "compact", "unknown" - """ + Tuple (str): tuple of hostname/drs prefix, guid, string: one of "hostname", "compact", "unknown" + """ # determine if hostname or compact identifier or unknown - drs_regex = r"drs://([A-Za-z0-9\.\-\~]+)/([A-Za-z0-9\.\-\_\~\/]+)" + drs_regex = r"drs://([A-Za-z0-9\.\-\~]+)/([A-Za-z0-9\.\-\_\~\/]+)" # either a drs prefix: matches = re.findall(drs_regex, drs_candidate, re.UNICODE) if len(matches) == 1: # this could be a hostname DRS id hostname_regex = ( - r"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*" - r"([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" + r"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*" + r"([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" ) hostname_matches = re.findall(hostname_regex, matches[0][0], re.UNICODE) if len(hostname_matches) == 1: - return matches[0][0], matches[0][1], "hostname" + return matches[0][0], matches[0][1], "hostname" # possible compact rep - compact_regex = r"([A-Za-z0-9\.\-\~]+)/([A-Za-z0-9\.\-\_\~\/]+)" + compact_regex = r"([A-Za-z0-9\.\-\~]+)/([A-Za-z0-9\.\-\_\~\/]+)" matches = re.findall(compact_regex, drs_candidate, re.UNICODE) if len(matches) == 1 and len(matches[0]) == 2: - return matches[0][0], matches[0][1], "compact" + return matches[0][0], matches[0][1], "compact" - # can't figure out a this identifier - return "", "", "unknown" + # can't figure out a this identifier + return "", "", "unknown" def resolve_drs_hostname_from_id( @@ -700,7 +700,7 @@

    Source code for gen3.tools.download.drs_download

    resolved_drs_prefix_cache: dict, mds_url: str, ) -> Optional[Tuple[str, str, str]]: - """Resolves and returns a DRS identifier + """Resolves and returns a DRS identifier The resolved_drs_prefix_cache is updated if needed and is a potential side effect of this call Args: @@ -710,13 +710,13 @@

    Source code for gen3.tools.download.drs_download

    Returns: the hostname of the DRS server if resolved, otherwise it returns None - """ + """ hostname = None prefix, identifier, identifier_type = parse_drs_identifier(object_id) - if identifier_type == "hostname": + if identifier_type == "hostname": return prefix, identifier, identifier_type - if identifier_type == "compact": + if identifier_type == "compact": if prefix not in resolved_drs_prefix_cache: hostname = resolve_drs(prefix, object_id, metadata_service_url=mds_url) if hostname is not None: @@ -733,14 +733,14 @@

    Source code for gen3.tools.download.drs_download

    mds_url: str, commons_url: str = None, ) -> None: - """Given a list of object_ids go through list and resolve + cache any unknown hosts + """Given a list of object_ids go through list and resolve + cache any unknown hosts Args: object_ids (List[Downloadable]): list of object to resolve resolved_drs_prefix_cache (dict): cache of resolved DRS prefixes mds_url (str): Gen3 metadata service to resolve DRS prefixes hostname (str): Hostname to main Gen3 environment - """ + """ for entry in object_ids: if commons_url is not None: entry.hostname = commons_url @@ -750,20 +750,20 @@

    Source code for gen3.tools.download.drs_download

    entry.object_id, resolved_drs_prefix_cache, mds_url ) if ( - drs_type == "hostname" + drs_type == "hostname" ): # drs_type is a hostname so object id will be the GUID entry.object_id = nid def ensure_dirpath_exists(path: Path) -> Path: - """Utility to create a directory if missing. + """Utility to create a directory if missing. Returns the path so that the call can be inlined in a another call Args: path (Path): path to create Returns path of created directory - """ + """ assert path out_path: Path = path @@ -776,7 +776,7 @@

    Source code for gen3.tools.download.drs_download

    def get_download_url_using_drs( drs_hostname: str, object_id: str, access_method: str, access_token: str ) -> Tuple[Optional[int], Optional[str]]: - """ + """ Returns the presigned URL for a DRS object, from a DRS hostname, via the access method Args: drs_hostname (str): hostname of DRS server @@ -787,86 +787,86 @@

    Source code for gen3.tools.download.drs_download

    Returns: presigned url to object status code - """ + """ headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": "bearer " + access_token, + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": "bearer " + access_token, } try: response = requests.get( - url=f"https://{drs_hostname}/ga4gh/drs/v1/objects/{object_id}/access/{access_method}", + url=f"https://{drs_hostname}/ga4gh/drs/v1/objects/{object_id}/access/{access_method}", headers=headers, ) response.raise_for_status() data = response.json() - return data.get("url", None), response.status_code + return data.get("url", None), response.status_code except requests.exceptions.Timeout: - logger.critical(f"Was unable to download: {object_id}. Timeout Error.") + logger.critical(f"Was unable to download: {object_id}. Timeout Error.") except requests.exceptions.HTTPError as exc: logger.critical( - f"HTTP Error ({exc.response.status_code}) when requesting download url from {access_method}" + f"HTTP Error ({exc.response.status_code}) when requesting download url from {access_method}" ) return None, exc.response.status_code return None, None def get_user_auth(commons_url: str, access_token: str) -> Optional[List[str]]: - """ - Retrieves a user's authz for the commons based on the access token. + """ + Retrieves a user's authz for the commons based on the access token. Any error will be logged and None is returned Args: commons_url (str): hostname of Gen3 indexd - access_token (str): user's auth token + access_token (str): user's auth token Returns: The authz object from the user endpoint - """ + """ headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": "bearer " + access_token, + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": "bearer " + access_token, } try: - response = requests.get(url=f"https://{commons_url}/user/user", headers=headers) + response = requests.get(url=f"https://{commons_url}/user/user", headers=headers) response.raise_for_status() data = response.json() - authz = data["authz"] + authz = data["authz"] return authz except requests.exceptions.HTTPError as exc: - logger.critical(f"HTTP Error ({exc.response.status_code}): getting user access") + logger.critical(f"HTTP Error ({exc.response.status_code}): getting user access") return None def list_auth(hostname: str, authz: dict): - """ + """ Prints the authz for a DRS hostname Args: hostname (str): hostname to list access authz (str): dictionary of authz stringts - """ + """ print( - "───────────────────────────────────────────────────────────────────────────────────────────────────────" + "───────────────────────────────────────────────────────────────────────────────────────────────────────" ) - print(f"Access for {hostname}:") + print(f"Access for {hostname}:") if authz is not None and len(authz) > 0: for access, methods in authz.items(): print( - f" {access : <55}: {' '.join(dict.fromkeys(x['method'] for x in methods).keys()):>40}" + f" {access : <55}: {' '.join(dict.fromkeys(x['method'] for x in methods).keys()):>40}" ) else: - print(" No access") + print(" No access") def get_hostname_from_endpoint(endpoint: str): - """ + """ Get hostname from an Gen3Auth endpoint value Args: endpoint (str): endpoint value form Gen3Auth - """ + """ if not endpoint: return None urlparts = urlparse(endpoint) @@ -876,10 +876,10 @@

    Source code for gen3.tools.download.drs_download

    [docs] class DownloadManager: - """ + """ Class to assist in downloading a list of Downloadable object which at a minimum is a json manifest of DRS object ids. The methods of interest are download and user_access. - """ + """ def __init__( self, @@ -889,7 +889,7 @@

    Source code for gen3.tools.download.drs_download

    show_progress: bool = False, commons_url: str = None, ): - """ + """ Initialize the DownloadManager so that is ready to start downloading. Note the downloadable objects are required so that all tokens are available to support the download. @@ -898,7 +898,7 @@

    Source code for gen3.tools.download.drs_download

    hostname (str): Gen3 commons home commons auth (Gen3Auth) : Gen3 authentication download_list (List[Downloadable]): list of objects to download - """ + """ self.hostname = ( hostname if hostname else get_hostname_from_endpoint(auth.endpoint) @@ -914,7 +914,7 @@

    Source code for gen3.tools.download.drs_download

    hostname=self.hostname, access_token=self.access_token, use_wts=False, - expire=datetime.fromtimestamp(decode_token(self.access_token)["exp"]), + expire=datetime.fromtimestamp(decode_token(self.access_token)["exp"]), ) } self.download_list = download_list @@ -923,22 +923,22 @@

    Source code for gen3.tools.download.drs_download

    [docs] def resolve_objects(self, object_list: List[Downloadable], show_progress: bool): - """ + """ Given an Downloadable object list, resolve the DRS hostnames and update each Downloadable Args: object_list (List[Downloadable]): list of Downloadable objects to resolve - """ + """ resolve_objects_drs_hostname( object_list, self.resolved_compact_drs, - mds_url=f"http://{self.hostname}/mds/aggregate/info" + mds_url=f"http://{self.hostname}/mds/aggregate/info" if self.hostname else None, commons_url=self.commons_url, ) progress_bar = ( - tqdm(desc=f"Resolving objects", total=len(object_list)) + tqdm(desc=f"Resolving objects", total=len(object_list)) if show_progress else InvisibleProgress() ) @@ -952,9 +952,9 @@

    Source code for gen3.tools.download.drs_download

    [docs] def cache_hosts_wts_tokens(self, object_list): - """ - Using the list of DRS host obtain a WTS token for all DRS hosts in the list. It's is possible - """ + """ + Using the list of DRS host obtain a WTS token for all DRS hosts in the list. It's is possible + """ # create two sets: one of the know WTS host and the other of the host in the manifest wts_endpoint_set = set(self.wts_endpoints.keys()) object_id_hostnames = { @@ -970,12 +970,12 @@

    Source code for gen3.tools.download.drs_download

    for drs_hostname in drs_in_wts: endpoint = KnownDRSEndpoint( hostname=drs_hostname, - idp=self.wts_endpoints[drs_hostname]["idp"], + idp=self.wts_endpoints[drs_hostname]["idp"], ) endpoint.renew_token(self.hostname, self.access_token) self.known_hosts[drs_hostname] = endpoint for drs_hostname in drs_not_in_wts: - # if we already know the host then we don't need to reset the host + # if we already know the host then we don't need to reset the host if drs_hostname in self.known_hosts: continue # mark hostname as unavailable @@ -983,22 +983,22 @@

    Source code for gen3.tools.download.drs_download

    hostname=drs_hostname, ) logger.critical( - f"Could not retrieve a token for {drs_hostname}: it is not available as a WTS endpoint." + f"Could not retrieve a token for {drs_hostname}: it is not available as a WTS endpoint." )
    [docs] def get_fresh_token(self, drs_hostname: str) -> Optional[str]: - """Will return and/or refresh and return a WTS token if hostname is known otherwise returns None. + """Will return and/or refresh and return a WTS token if hostname is known otherwise returns None. Args: drs_hostname (str): hostname to get token for Returns: access token if successful otherwise None - """ + """ if drs_hostname not in self.known_hosts: - logger.critical(f"Could not find {drs_hostname} in cache.") + logger.critical(f"Could not find {drs_hostname} in cache.") return None if self.known_hosts[drs_hostname].available: if not self.known_hosts[drs_hostname].expired(): @@ -1018,12 +1018,12 @@

    Source code for gen3.tools.download.drs_download

    def download( self, object_list: List[Downloadable], - save_directory: str = ".", + save_directory: str = ".", show_progress: bool = False, unpack_packages: bool = True, delete_unpacked_packages: bool = False, ) -> Dict[str, Any]: - """ + """ Downloads objects to the directory or current working directory. The input is an list of Downloadable object created by loading a manifest using the Manifest class or a call to Manifest.load(... @@ -1043,7 +1043,7 @@

    Source code for gen3.tools.download.drs_download

    Returns: List of DownloadStatus objects for each object id in object_list - """ + """ self.cache_hosts_wts_tokens(object_list) output_dir = Path(save_directory) @@ -1072,23 +1072,23 @@

    Source code for gen3.tools.download.drs_download

    if entry.hostname is None: logger.critical( - f"Unable to resolve, skipping {entry.object_id}. Skipping" + f"Unable to resolve, skipping {entry.object_id}. Skipping" ) - completed[entry.object_id].status = "error (resolving DRS host)" + completed[entry.object_id].status = "error (resolving DRS host)" continue # check to see if we have tokens if entry.hostname not in self.known_hosts: logger.critical( - f"{entry.hostname} is not present in this commons remote user access. Skipping {entry.file_name}" + f"{entry.hostname} is not present in this commons remote user access. Skipping {entry.file_name}" ) - completed[entry.object_id].status = "error (resolving DRS host)" + completed[entry.object_id].status = "error (resolving DRS host)" continue if self.known_hosts[entry.hostname].available is False: logger.critical( - f"Was unable to get user authorization from {entry.hostname}. Skipping {entry.file_name}" + f"Was unable to get user authorization from {entry.hostname}. Skipping {entry.file_name}" ) - completed[entry.object_id].status = "error (no auth)" + completed[entry.object_id].status = "error (no auth)" continue drs_hostname = entry.hostname @@ -1096,18 +1096,18 @@

    Source code for gen3.tools.download.drs_download

    if access_token is None: logger.critical( - f"No access token defined for {entry.object_id}. Skipping" + f"No access token defined for {entry.object_id}. Skipping" ) - completed[entry.object_id].status = "error (no access token)" + completed[entry.object_id].status = "error (no access token)" continue # TODO refine the selection of access_method if len(entry.access_methods) == 0: logger.critical( - f"No access methods defined for {entry.object_id}. Skipping" + f"No access methods defined for {entry.object_id}. Skipping" ) - completed[entry.object_id].status = "error (no access methods)" + completed[entry.object_id].status = "error (no access methods)" continue - access_method = entry.access_methods[0]["access_id"] + access_method = entry.access_methods[0]["access_id"] download_url, status_code = get_download_url_using_drs( drs_hostname, @@ -1119,7 +1119,7 @@

    Source code for gen3.tools.download.drs_download

    if download_url is None: if status_code != 200: completed[entry.object_id].status_code = status_code - completed[entry.object_id].status = "error" + completed[entry.object_id].status = "error" continue completed[entry.object_id].start_time = datetime.now(timezone.utc) @@ -1136,29 +1136,29 @@

    Source code for gen3.tools.download.drs_download

    except Exception: mds_entry = {} # no MDS or object not in MDS logger.debug( - f"{entry.file_name} is not a package and will not be expanded" + f"{entry.file_name} is not a package and will not be expanded" ) - # if the metadata type is "package", then unpack - if mds_entry.get("type") == "package": + # if the metadata type is "package", then unpack + if mds_entry.get("type") == "package": try: unpackage_object(filepath) except Exception as e: logger.critical( - f"{entry.file_name} had an issue while being unpackaged: {e}" + f"{entry.file_name} had an issue while being unpackaged: {e}" ) res = False if delete_unpacked_packages: filepath.unlink() if res: - completed[entry.object_id].status = "downloaded" + completed[entry.object_id].status = "downloaded" logger.debug( - f"object {entry.object_id} has been successfully downloaded." + f"object {entry.object_id} has been successfully downloaded." ) else: - completed[entry.object_id].status = "error" - logger.debug(f"object {entry.object_id} has failed to be downloaded.") + completed[entry.object_id].status = "error" + logger.debug(f"object {entry.object_id} has failed to be downloaded.") completed[entry.object_id].end_time = datetime.now(timezone.utc) return completed
    @@ -1167,20 +1167,20 @@

    Source code for gen3.tools.download.drs_download

    [docs] def user_access(self): - """ - List the user's access permissions on each host needed to download + """ + List the user's access permissions on each host needed to download DRS objects in the manifest. A useful way to determine if access permissions are one reason a download failed. Returns: list of authz for each DRS host - """ + """ results = {} self.cache_hosts_wts_tokens(self.download_list) for hostname in self.known_hosts.keys(): if self.known_hosts[hostname].available is False: logger.critical( - f"Was unable to get user authorization from {hostname}." + f"Was unable to get user authorization from {hostname}." ) continue access_token = self.known_hosts[hostname].access_token @@ -1196,13 +1196,13 @@

    Source code for gen3.tools.download.drs_download

    hostname, auth, infile, - output_dir=".", + output_dir=".", show_progress=False, unpack_packages=True, delete_unpacked_packages=False, commons_url=None, ) -> Optional[Dict[str, Any]]: - """ + """ A convenience function used to download a json manifest. Args: hostname (str): hostname of Gen3 commons to use for access and WTS @@ -1215,15 +1215,15 @@

    Source code for gen3.tools.download.drs_download

    Returns: List of DownloadStatus objects for each object id in object_list - """ + """ object_list = Manifest.load(Path(infile)) if object_list is None: - logger.critical(f"Error loading {infile}") + logger.critical(f"Error loading {infile}") return None try: auth.get_access_token() except Gen3AuthError: - logger.critical(f"Unable to authenticate your credentials with {hostname}") + logger.critical(f"Unable to authenticate your credentials with {hostname}") return downloader = DownloadManager( @@ -1248,13 +1248,13 @@

    Source code for gen3.tools.download.drs_download

    hostname, auth, object_ids, - output_dir=".", + output_dir=".", show_progress=False, unpack_packages=True, delete_unpacked_packages=False, commons_url=None, ) -> Optional[Dict[str, Any]]: - """ + """ A convenience function used to download a single DRS object. Args: hostname (str): hostname of Gen3 commons to use for access and WTS @@ -1267,11 +1267,11 @@

    Source code for gen3.tools.download.drs_download

    Returns: List of DownloadStatus objects for the DRS object - """ + """ try: auth.get_access_token() except Gen3AuthError: - logger.critical(f"Unable to authenticate your credentials with {hostname}") + logger.critical(f"Unable to authenticate your credentials with {hostname}") return None object_list = [Downloadable(object_id=object_id) for object_id in object_ids] @@ -1294,7 +1294,7 @@

    Source code for gen3.tools.download.drs_download

    def _listfiles(hostname, auth, infile: str) -> bool: - """ + """ A wrapper function used by the cli to list files in a manifest. Args: hostname (str): hostname of Gen3 commons to use for access and WTS @@ -1303,7 +1303,7 @@

    Source code for gen3.tools.download.drs_download

    Returns: True if successfully listed - """ + """ object_list = Manifest.load(Path(infile)) if object_list is None: return False @@ -1311,11 +1311,11 @@

    Source code for gen3.tools.download.drs_download

    try: auth.get_access_token() except Gen3AuthError: - logger.critical(f"Unable to authenticate your credentials with {hostname}") + logger.critical(f"Unable to authenticate your credentials with {hostname}") return False except requests.exceptions.RequestException as ex: logger.critical( - f"Unable to authenticate your credentials with {hostname}: {str(ex)}" + f"Unable to authenticate your credentials with {hostname}: {str(ex)}" ) return False @@ -1330,7 +1330,7 @@

    Source code for gen3.tools.download.drs_download

    def _list_object(hostname, auth, object_id: str) -> bool: - """ + """ Lists a DRS object. Args: hostname (str): hostname of Gen3 commons to use for access and WTS @@ -1339,15 +1339,15 @@

    Source code for gen3.tools.download.drs_download

    Returns: True if successfully listed - """ + """ try: auth.get_access_token() except Gen3AuthError: - logger.critical(f"Unable to authenticate your credentials with {hostname}") + logger.critical(f"Unable to authenticate your credentials with {hostname}") return False except requests.exceptions.RequestException as ex: logger.critical( - f"Unable to authenticate your credentials with {hostname}: {ex}" + f"Unable to authenticate your credentials with {hostname}: {ex}" ) return False @@ -1366,7 +1366,7 @@

    Source code for gen3.tools.download.drs_download

    def _list_access(hostname, auth, infile: str) -> bool: - """ + """ A convenience function to list a users access for all DRS hostname in a manifest. Args: hostname (str): hostname of Gen3 commons to use for access and WTS @@ -1375,7 +1375,7 @@

    Source code for gen3.tools.download.drs_download

    Returns: True if successfully listed - """ + """ object_list = Manifest.load(Path(infile)) if object_list is None: return False @@ -1383,11 +1383,11 @@

    Source code for gen3.tools.download.drs_download

    try: auth.get_access_token() except Gen3AuthError: - logger.critical(f"Unable to authenticate your credentials with {hostname}") + logger.critical(f"Unable to authenticate your credentials with {hostname}") return False except requests.exceptions.RequestException as ex: logger.critical( - f"Unable to authenticate your credentials with {hostname}: {ex}" + f"Unable to authenticate your credentials with {hostname}: {ex}" ) return False @@ -1401,11 +1401,11 @@

    Source code for gen3.tools.download.drs_download

    return True -# These functions are exposed to the SDK's cli under the drs-pull subcommand +# These functions are exposed to the SDK's cli under the drs-pull subcommand
    [docs] def list_files_in_drs_manifest(hostname, auth, infile: str) -> bool: - """ + """ A wrapper function used by the cli to list files in a manifest. Args: hostname (str): hostname of Gen3 commons to use for access and WTS @@ -1414,7 +1414,7 @@

    Source code for gen3.tools.download.drs_download

    Returns: True if successfully listed - """ + """ return _listfiles(hostname, auth, infile)
    @@ -1422,7 +1422,7 @@

    Source code for gen3.tools.download.drs_download

    [docs] def list_drs_object(hostname, auth, object_id: str) -> bool: - """ + """ A convenience function used to list a DRS object. Args: hostname (str): hostname of Gen3 commons to use for access and WTS @@ -1431,7 +1431,7 @@

    Source code for gen3.tools.download.drs_download

    Returns: True if successfully listed - """ + """ return _list_object(hostname, auth, object_id) # pragma: no cover
    @@ -1448,7 +1448,7 @@

    Source code for gen3.tools.download.drs_download

    delete_unpacked_packages=False, commons_url=None, ) -> None: - """ + """ A convenience function used to download a json manifest. Args: hostname (str): hostname of Gen3 commons to use for access and WTS @@ -1459,7 +1459,7 @@

    Source code for gen3.tools.download.drs_download

    delete_unpacked_packages (bool): set to True to delete package files after unpacking them Returns: - """ + """ _download( hostname, auth, @@ -1483,7 +1483,7 @@

    Source code for gen3.tools.download.drs_download

    delete_unpacked_packages=False, commons_url=None, ) -> None: - """ + """ A convenience function used to download a single DRS object. Args: hostname (str): hostname of Gen3 commons to use for access and WTS @@ -1495,7 +1495,7 @@

    Source code for gen3.tools.download.drs_download

    Returns: List of DownloadStatus objects for the DRS object - """ + """ return _download_obj( hostname, auth, @@ -1511,7 +1511,7 @@

    Source code for gen3.tools.download.drs_download

    [docs] def list_access_in_drs_manifest(hostname, auth, infile) -> bool: - """ + """ A convenience function to list a users access for all DRS hostname in a manifest. Args: hostname (str): hostname of Gen3 commons to use for access and WTS @@ -1520,7 +1520,7 @@

    Source code for gen3.tools.download.drs_download

    Returns: True if successfully listed - """ + """ return _list_access(hostname, auth, infile)
    diff --git a/docs/_build/html/_modules/gen3/tools/indexing/download_manifest.html b/docs/_build/html/_modules/gen3/tools/indexing/download_manifest.html index ad9bd1fb2..a9e34fb9c 100644 --- a/docs/_build/html/_modules/gen3/tools/indexing/download_manifest.html +++ b/docs/_build/html/_modules/gen3/tools/indexing/download_manifest.html @@ -31,10 +31,10 @@

    Source code for gen3.tools.indexing.download_manifest

    -"""
    +"""
     Module for indexing actions for downloading a manifest of
    -indexed file objects (against indexd's API). Supports
    -multiple processes and coroutines using Python's asyncio library.
    +indexed file objects (against indexd's API). Supports
    +multiple processes and coroutines using Python's asyncio library.
     
     The default manifest format created is a Comma-Separated Value file (csv)
     with rows for every record. A header row is created with field names:
    @@ -48,11 +48,11 @@ 

    Source code for gen3.tools.indexing.download_manifest

    MAX_CONCURRENT_REQUESTS (int): maximum number of desired concurrent requests across processes/threads TMP_FOLDER (str): Folder directory for placing temporary files - NOTE - We have to use a temporary folder b/c Python's file writing is not - thread-safe so we can't have all processes writing to the same file. + NOTE - We have to use a temporary folder b/c Python's file writing is not + thread-safe so we can't have all processes writing to the same file. To workaround this, we have each process write to a file and concat them all post-processing. -""" +""" import asyncio import aiofiles import click @@ -86,7 +86,7 @@

    Source code for gen3.tools.indexing.download_manifest

    INDEXD_RECORD_PAGE_SIZE = 1024 MAX_CONCURRENT_REQUESTS = 24 CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) -TMP_FOLDER = os.path.abspath(CURRENT_DIR + "/tmp") + "/" +TMP_FOLDER = os.path.abspath(CURRENT_DIR + "/tmp") + "/" logging = get_logger(__name__) @@ -95,13 +95,13 @@

    Source code for gen3.tools.indexing.download_manifest

    [docs] async def async_download_object_manifest( commons_url, - output_filename="object-manifest.csv", + output_filename="object-manifest.csv", num_processes=4, max_concurrent_requests=MAX_CONCURRENT_REQUESTS, input_manifest=None, - python_subprocess_command="python", + python_subprocess_command="python", ): - """ + """ Download all file object records into a manifest csv Args: @@ -118,27 +118,27 @@

    Source code for gen3.tools.indexing.download_manifest

    python_subprocess_command (str, optional): Command used to execute a python process. By default you should not need to change this, but if you are running something like MacOS and only installed Python 3.x - you may need to specify "python3". - """ + you may need to specify "python3". + """ start_time = time.perf_counter() - logging.info(f"start time: {start_time}") + logging.info(f"start time: {start_time}") # ensure tmp directories exists and are empty os.makedirs(TMP_FOLDER, exist_ok=True) - os.makedirs(TMP_FOLDER + "input", exist_ok=True) - os.makedirs(TMP_FOLDER + "output", exist_ok=True) + os.makedirs(TMP_FOLDER + "input", exist_ok=True) + os.makedirs(TMP_FOLDER + "output", exist_ok=True) for file in os.listdir(TMP_FOLDER): file_path = os.path.join(TMP_FOLDER, file) if os.path.isfile(file_path): os.unlink(file_path) - for file in os.listdir(TMP_FOLDER + "input"): - file_path = os.path.join(TMP_FOLDER + "input", file) + for file in os.listdir(TMP_FOLDER + "input"): + file_path = os.path.join(TMP_FOLDER + "input", file) if os.path.isfile(file_path): os.unlink(file_path) - for file in os.listdir(TMP_FOLDER + "output"): - file_path = os.path.join(TMP_FOLDER + "output", file) + for file in os.listdir(TMP_FOLDER + "output"): + file_path = os.path.join(TMP_FOLDER + "output", file) if os.path.isfile(file_path): os.unlink(file_path) @@ -152,8 +152,8 @@

    Source code for gen3.tools.indexing.download_manifest

    ) end_time = time.perf_counter() - logging.info(f"end time: {end_time}") - logging.info(f"run time: {end_time-start_time}")
    + logging.info(f"end time: {end_time}") + logging.info(f"run time: {end_time-start_time}")
    @@ -165,7 +165,7 @@

    Source code for gen3.tools.indexing.download_manifest

    input_manifest, python_subprocess_command, ): - """ + """ Spins up number of processes provided to parse indexd records and eventually write to a single output file manifest. @@ -180,7 +180,7 @@

    Source code for gen3.tools.indexing.download_manifest

    input_manifest (str): Input file. Read available object data from objects in this file instead of reading everything in indexd. This will attempt to query indexd for only the records identified in this manifest. - """ + """ # used when requesting all records page_chunks = [] @@ -190,40 +190,40 @@

    Source code for gen3.tools.indexing.download_manifest

    if input_manifest: # create chunks of checksums - logging.debug(f"parsing input file {input_manifest}") + logging.debug(f"parsing input file {input_manifest}") input_records, headers = get_and_verify_fileinfos_from_manifest(input_manifest) num_input_records = len(input_records) if not num_input_records: raise AttributeError( - f"No valid records found in provided input file: {input_manifest}. " - "Please check previous logs." + f"No valid records found in provided input file: {input_manifest}. " + "Please check previous logs." ) - logging.debug(f"number input_records: {num_input_records}") - logging.debug(f"num processes: {num_processes}") + logging.debug(f"number input_records: {num_input_records}") + logging.debug(f"num processes: {num_processes}") # batch records into subprocesses chunks chunk_size = int(math.ceil(float(num_input_records) / num_processes)) - logging.debug(f"records chunk size: {chunk_size}") + logging.debug(f"records chunk size: {chunk_size}") record_chunks = list(yield_chunks(input_records, chunk_size)) else: index = Gen3Index(commons_url) - logging.debug(f"requesting indexd stats...") - num_files = int(index.get_stats().get("fileCount")) - logging.debug(f"number files: {num_files}") + logging.debug(f"requesting indexd stats...") + num_files = int(index.get_stats().get("fileCount")) + logging.debug(f"number files: {num_files}") # paging is 0-based, so subtract 1 from ceiling # note: float() is necessary to force Python 3 to not floor the result max_page = int(math.ceil(float(num_files) / INDEXD_RECORD_PAGE_SIZE)) - 1 - logging.debug(f"max page: {max_page}") - logging.debug(f"num processes: {num_processes}") + logging.debug(f"max page: {max_page}") + logging.debug(f"num processes: {num_processes}") pages = [x for x in range(max_page + 1)] # batch pages into subprocesses chunk_size = int(math.ceil(float(len(pages)) / num_processes)) - logging.debug(f"page chunk size: {chunk_size}") + logging.debug(f"page chunk size: {chunk_size}") if chunk_size: page_chunks = [ @@ -232,31 +232,31 @@

    Source code for gen3.tools.indexing.download_manifest

    processes = [] for x in range(max(len(page_chunks), len(record_chunks))): - pages = ",".join(map(str, page_chunks[x])) if page_chunks else "," + pages = ",".join(map(str, page_chunks[x])) if page_chunks else "," input_record_chunks = ( - "|||".join(map(json.dumps, record_chunks[x])) if record_chunks else "|||" + "|||".join(map(json.dumps, record_chunks[x])) if record_chunks else "|||" ) # write record_checksum chunks to temporary files since the size can overload # command line arguments - input_records_chunk_filename = TMP_FOLDER + f"input/input_records_chunk_{x}.txt" + input_records_chunk_filename = TMP_FOLDER + f"input/input_records_chunk_{x}.txt" logging.info( - f"writing input_record_chunks chunk {x} into {input_records_chunk_filename}" + f"writing input_record_chunks chunk {x} into {input_records_chunk_filename}" ) - with open(input_records_chunk_filename, "wb") as outfile: - outfile.write(input_record_chunks.encode("utf8")) + with open(input_records_chunk_filename, "wb") as outfile: + outfile.write(input_record_chunks.encode("utf8")) # call the cli function below and pass in chunks of pages for each process command = ( - f"{python_subprocess_command} {CURRENT_DIR}/download_manifest.py --commons_url " - f"{commons_url} --pages {pages} --input-record-chunks-file {input_records_chunk_filename} " - f"--num_processes {num_processes} --max_concurrent_requests {max_concurrent_requests}" + f"{python_subprocess_command} {CURRENT_DIR}/download_manifest.py --commons_url " + f"{commons_url} --pages {pages} --input-record-chunks-file {input_records_chunk_filename} " + f"--num_processes {num_processes} --max_concurrent_requests {max_concurrent_requests}" ) logging.info(command) process = await asyncio.create_subprocess_shell(command) - logging.info(f"Process_{process.pid} - Started w/: {command}") + logging.info(f"Process_{process.pid} - Started w/: {command}") processes.append(process) for process in processes: @@ -264,59 +264,59 @@

    Source code for gen3.tools.indexing.download_manifest

    stdout, stderr = await process.communicate() if process.returncode == 0: - logging.info(f"Process_{process.pid} - Done") + logging.info(f"Process_{process.pid} - Done") else: - logging.info(f"Process_{process.pid} - FAILED") + logging.info(f"Process_{process.pid} - FAILED") - logging.info(f"done processing, combining outputs to single file {output_filename}") + logging.info(f"done processing, combining outputs to single file {output_filename}") # remove existing output if it exists if os.path.isfile(output_filename): os.unlink(output_filename) - with open(output_filename, "wb") as outfile: - outfile.write("guid,urls,authz,acl,md5,file_size,file_name\n".encode("utf8")) - for filename in glob.glob(TMP_FOLDER + "output/*"): + with open(output_filename, "wb") as outfile: + outfile.write("guid,urls,authz,acl,md5,file_size,file_name\n".encode("utf8")) + for filename in glob.glob(TMP_FOLDER + "output/*"): if output_filename == filename: - # don't want to copy the output into the output + # don't want to copy the output into the output continue - logging.info(f"combining {filename} into {output_filename}") - with open(filename, "rb") as readfile: + logging.info(f"combining {filename} into {output_filename}") + with open(filename, "rb") as readfile: shutil.copyfileobj(readfile, outfile) - logging.info(f"done writing output to file {output_filename}") + logging.info(f"done writing output to file {output_filename}") @click.command() @click.option( - "--commons_url", help="Root domain (url) for a commons containing indexd." + "--commons_url", help="Root domain (url) for a commons containing indexd." ) @click.option( - "--pages", - help='Comma-Separated string of integers representing pages. ex: "2,4,5,6"', + "--pages", + help='Comma-Separated string of integers representing pages. ex: "2,4,5,6"', ) @click.option( - "--input-record-chunks-file", + "--input-record-chunks-file", help=( - "File containing delimited string of records to retrieve." "ex: /foo/bar.txt" + "File containing delimited string of records to retrieve." "ex: /foo/bar.txt" ), ) @click.option( - "--num_processes", + "--num_processes", type=int, - help="number of processes you are running so we can make sure we don't open " - 'too many http connections. ex: "4"', + help="number of processes you are running so we can make sure we don't open " + 'too many http connections. ex: "4"', ) @click.option( - "--max_concurrent_requests", + "--max_concurrent_requests", type=int, - help="number of processes you are running so we can make sure we don't open " - 'too many http connections. ex: "4"', + help="number of processes you are running so we can make sure we don't open " + 'too many http connections. ex: "4"', ) def write_page_records_to_files( commons_url, pages, input_record_chunks_file, num_processes, max_concurrent_requests ): - """ + """ Command line interface function for requesting a number of records from indexd and writing to a file in that process. num_processes is only used to calculate how many open connections this process should request. @@ -335,34 +335,34 @@

    Source code for gen3.tools.indexing.download_manifest

    Raises: AttributeError: No pages specified to get records from - """ + """ if not pages and not input_record_chunks_file: raise AttributeError( - "No info specified to get records with. " - "Supply either pages or input-record-chunks-file" + "No info specified to get records with. " + "Supply either pages or input-record-chunks-file" ) - pages = [item for item in pages.strip().strip(",").split(",") if item] + pages = [item for item in pages.strip().strip(",").split(",") if item] input_record_chunks = [] if input_record_chunks_file: - with open(input_record_chunks_file, "r", encoding="utf8") as file: - input_record_chunks_from_file = "|||".join(file.readlines()) + with open(input_record_chunks_file, "r", encoding="utf8") as file: + input_record_chunks_from_file = "|||".join(file.readlines()) input_record_chunks = [ json.loads(item) - for item in input_record_chunks_from_file.strip().split("|||") + for item in input_record_chunks_from_file.strip().split("|||") if item ] if not pages and not input_record_chunks: raise AttributeError( - "No info specified to get records with. " - "Supply either pages or input-record-chunks-file with records in the file. " + "No info specified to get records with. " + "Supply either pages or input-record-chunks-file with records in the file. " ) if pages and input_record_chunks: raise AttributeError( - "YOU MUST USE EITHER `pages` OR `input-record-chunks-file`, YOU CANNOT USE BOTH. " - f"You provided pages={pages} and input-record-chunks-file={input_record_chunks_file}." + "YOU MUST USE EITHER `pages` OR `input-record-chunks-file`, YOU CANNOT USE BOTH. " + f"You provided pages={pages} and input-record-chunks-file={input_record_chunks_file}." ) loop = get_or_create_event_loop_for_thread() @@ -382,14 +382,14 @@

    Source code for gen3.tools.indexing.download_manifest

    async def _get_records_and_write_to_file( commons_url, pages, input_record_chunks, num_processes, max_concurrent_requests ): - """ + """ Getting indexd records and writing to a file. This function creates semaphores to limit the number of concurrent http connections that get opened to send requests to indexd. It then uses asyncio to start a number of coroutines. Steps: 1) requests to indexd to get records (writes resulting records to a queue) - 2) puts a final "DONE" in the queue to stop coroutine that will read from queue + 2) puts a final "DONE" in the queue to stop coroutine that will read from queue 3) reading those records from the queue and writing to a file Args: @@ -398,15 +398,15 @@

    Source code for gen3.tools.indexing.download_manifest

    input_record_chunks (List[dict]): List of indexd records to request num_processes (int): number of concurrent processes being requested (including this one) - """ + """ max_requests = int(max_concurrent_requests / num_processes) - logging.debug(f"max concurrent requests per process: {max_requests}") + logging.debug(f"max concurrent requests per process: {max_requests}") lock = asyncio.Semaphore(max_requests) queue = asyncio.Queue() write_to_file_task = asyncio.ensure_future(_parse_from_queue(queue)) if pages: - logging.debug("putting records from page into queue") + logging.debug("putting records from page into queue") await asyncio.gather( *( _put_records_from_page_in_queue(page, commons_url, lock, queue) @@ -414,7 +414,7 @@

    Source code for gen3.tools.indexing.download_manifest

    ) ) else: - logging.debug("putting records from input manifest into queue") + logging.debug("putting records from input manifest into queue") await asyncio.gather( *( _put_records_from_input_manifest_in_queue( @@ -424,14 +424,14 @@

    Source code for gen3.tools.indexing.download_manifest

    ) ) - await queue.put("DONE") + await queue.put("DONE") await write_to_file_task async def _put_records_from_input_manifest_in_queue( input_record, commons_url, lock, queue ): - """ + """ Gets a semaphore then requests records for the given input_record and puts them in a queue. @@ -441,14 +441,14 @@

    Source code for gen3.tools.indexing.download_manifest

    lock (asyncio.Semaphore): semaphones used to limit ammount of concurrent http connections queue (asyncio.Queue): queue to put indexd records in - """ + """ checksum = input_record.get(MD5_STANDARD_KEY) index = Gen3Index(commons_url) async with lock: - # default ssl handling unless it's explicitly http:// + # default ssl handling unless it's explicitly http:// ssl = None - if "https" not in commons_url: + if "https" not in commons_url: ssl = False records = await index.async_get_records_from_checksum( @@ -463,7 +463,7 @@

    Source code for gen3.tools.indexing.download_manifest

    async def _put_records_from_page_in_queue(page, commons_url, lock, queue): - """ + """ Gets a semaphore then requests records for the given page and puts them in a queue. @@ -473,12 +473,12 @@

    Source code for gen3.tools.indexing.download_manifest

    lock (asyncio.Semaphore): semaphones used to limit ammount of concurrent http connections queue (asyncio.Queue): queue to put indexd records in - """ + """ index = Gen3Index(commons_url) async with lock: - # default ssl handling unless it's explicitly http:// + # default ssl handling unless it's explicitly http:// ssl = None - if "https" not in commons_url: + if "https" not in commons_url: ssl = False records = await index.async_get_records_on_page( @@ -488,69 +488,69 @@

    Source code for gen3.tools.indexing.download_manifest

    async def _parse_from_queue(queue): - """ + """ Read from the queue and write to a file Args: queue (asyncio.Queue): queue to read indexd records from - """ + """ loop = get_or_create_event_loop_for_thread() - file_name = TMP_FOLDER + f"output/{os.getpid()}.csv" - async with aiofiles.open(file_name, "w+", encoding="utf8") as file: - logging.info(f"Writing to {file_name}") + file_name = TMP_FOLDER + f"output/{os.getpid()}.csv" + async with aiofiles.open(file_name, "w+", encoding="utf8") as file: + logging.info(f"Writing to {file_name}") csv_writer = csv.writer(file) records = await queue.get() - while records != "DONE": + while records != "DONE": if records: for record in list(records): # we want to represent records that are found correctly - # (e.g. ones with did's), but records that are directly from an input + # (e.g. ones with did's), but records that are directly from an input # manifest (e.g. no did) we do NOT want to modify, so # treat these cases separately - if record.get("did"): - urls = " ".join( + if record.get("did"): + urls = " ".join( sorted( [ - url.replace(" ", "%20") - for url in record.get("urls") + url.replace(" ", "%20") + for url in record.get("urls") if url ] ) ) - authz = " ".join( + authz = " ".join( sorted( [ - authz_resource.replace(" ", "%20") - for authz_resource in record.get("authz") + authz_resource.replace(" ", "%20") + for authz_resource in record.get("authz") if authz_resource ] ) ) - acl = " ".join( + acl = " ".join( sorted( - [a.replace(" ", "%20") for a in record.get("acl") if a] + [a.replace(" ", "%20") for a in record.get("acl") if a] ) ) manifest_row = [ - record.get("did", ""), + record.get("did", ""), urls, authz, acl, - record.get("hashes", {}).get("md5", ""), - record.get("size", ""), - record.get("file_name", ""), + record.get("hashes", {}).get("md5", ""), + record.get("size", ""), + record.get("file_name", ""), ] else: manifest_row = [ - record.get(GUID_STANDARD_KEY, ""), - record.get(URLS_STANDARD_KEY, ""), - record.get(AUTHZ_STANDARD_KEY, ""), - record.get(ACL_STANDARD_KEY, ""), - record.get(MD5_STANDARD_KEY, ""), - record.get(SIZE_STANDARD_KEY, ""), - record.get(FILENAME_STANDARD_KEY, ""), + record.get(GUID_STANDARD_KEY, ""), + record.get(URLS_STANDARD_KEY, ""), + record.get(AUTHZ_STANDARD_KEY, ""), + record.get(ACL_STANDARD_KEY, ""), + record.get(MD5_STANDARD_KEY, ""), + record.get(SIZE_STANDARD_KEY, ""), + record.get(FILENAME_STANDARD_KEY, ""), ] await csv_writer.writerow(manifest_row) @@ -559,7 +559,7 @@

    Source code for gen3.tools.indexing.download_manifest

    file.flush() -if __name__ == "__main__": +if __name__ == "__main__": write_page_records_to_files()
    diff --git a/docs/_build/html/_modules/gen3/tools/indexing/index_manifest.html b/docs/_build/html/_modules/gen3/tools/indexing/index_manifest.html index 142ed2ab3..d859b0830 100644 --- a/docs/_build/html/_modules/gen3/tools/indexing/index_manifest.html +++ b/docs/_build/html/_modules/gen3/tools/indexing/index_manifest.html @@ -31,8 +31,8 @@

    Source code for gen3.tools.indexing.index_manifest

    -"""
    -Module for indexing object files in a manifest (against indexd's API).
    +"""
    +Module for indexing object files in a manifest (against indexd's API).
     
     The default manifest format created is a Tab-Separated Value file (tsv)
     with rows for every record.
    @@ -43,11 +43,11 @@ 

    Source code for gen3.tools.indexing.index_manifest

    All supported formats of acl, authz and url fields are shown in the below example. guid md5 size acl authz url -255e396f-f1f8-11e9-9a07-0a80fada099c 473d83400bc1bc9dc635e334faddf33c 363455714 ['Open'] [s3://pdcdatastore/test1.raw] +255e396f-f1f8-11e9-9a07-0a80fada099c 473d83400bc1bc9dc635e334faddf33c 363455714 ['Open'] [s3://pdcdatastore/test1.raw] 255e396f-f1f8-11e9-9a07-0a80fada098c 473d83400bc1bc9dc635e334faddd33c 343434344 Open s3://pdcdatastore/test2.raw 255e396f-f1f8-11e9-9a07-0a80fada097c 473d83400bc1bc9dc635e334fadd433c 543434443 phs0001 phs0002 s3://pdcdatastore/test3.raw -255e396f-f1f8-11e9-9a07-0a80fada096c 473d83400bc1bc9dc635e334fadd433c 363455714 ['phs0001', 'phs0002'] ['s3://pdcdatastore/test4.raw'] -255e396f-f1f8-11e9-9a07-0a80fada010c 473d83400bc1bc9dc635e334fadde33c 363455714 ['Open'] s3://pdcdatastore/test5.raw +255e396f-f1f8-11e9-9a07-0a80fada096c 473d83400bc1bc9dc635e334fadd433c 363455714 ['phs0001', 'phs0002'] ['s3://pdcdatastore/test4.raw'] +255e396f-f1f8-11e9-9a07-0a80fada010c 473d83400bc1bc9dc635e334fadde33c 363455714 ['Open'] s3://pdcdatastore/test5.raw Attributes: CURRENT_DIR (str): directory this file is in @@ -60,9 +60,9 @@

    Source code for gen3.tools.indexing.index_manifest

    PREV_GUID (list(string)): supported previous guid column names Usages: - python index_manifest.py --commons_url https://giangb.planx-pla.net --manifest_file path_to_manifest --auth "admin,admin" --replace_urls False --thread_num 10 + python index_manifest.py --commons_url https://giangb.planx-pla.net --manifest_file path_to_manifest --auth "admin,admin" --replace_urls False --thread_num 10 python index_manifest.py --commons_url https://giangb.planx-pla.net --manifest_file path_to_manifest --api_key ./credentials.json --replace_urls False --thread_num 10 -""" +""" import os import csv import click @@ -104,9 +104,9 @@

    Source code for gen3.tools.indexing.index_manifest

    [docs] class ThreadControl(object): - """ + """ Class for thread synchronization - """ + """ def __init__(self, processed_files=0, num_total_files=0): self.mutexLock = threading.Lock() @@ -116,7 +116,7 @@

    Source code for gen3.tools.indexing.index_manifest

    def _write_csv(filename, files, fieldnames=None): - """ + """ write to csv file Args: @@ -124,24 +124,24 @@

    Source code for gen3.tools.indexing.index_manifest

    files(list(dict)): list of file info [ { - "guid": "guid_example", - "filename": "example", - "size": 100, - "acl": "['open']", - "md5": "md5_hash", + "guid": "guid_example", + "filename": "example", + "size": 100, + "acl": "['open']", + "md5": "md5_hash", }, ] fieldnames(list(str)): list of column names Returns: None - """ + """ if not files: return None fieldnames = fieldnames or files[0].keys() - with open(filename, mode="w") as outfile: - writer = csv.DictWriter(outfile, delimiter="\t", fieldnames=fieldnames) + with open(filename, mode="w") as outfile: + writer = csv.DictWriter(outfile, delimiter="\t", fieldnames=fieldnames) writer.writeheader() for f in files: @@ -159,7 +159,7 @@

    Source code for gen3.tools.indexing.index_manifest

    force_metadata_columns_even_if_empty, fi, ): - """ + """ Index a single file, and submit additional metadata to the metadata service if provided Args: @@ -174,49 +174,49 @@

    Source code for gen3.tools.indexing.index_manifest

    Returns: None - """ + """ index_success = True try: urls = ( get_urls(fi[URLS_STANDARD_KEY]) if URLS_STANDARD_KEY in fi - and fi[URLS_STANDARD_KEY] != "[]" + and fi[URLS_STANDARD_KEY] != "[]" and fi[URLS_STANDARD_KEY] else [] ) authz = ( [ - element.strip().replace("'", "").replace('"', "").replace("%20", " ") + element.strip().replace("'", "").replace('"', "").replace("%20", " ") for element in _standardize_str(fi[AUTHZ_STANDARD_KEY]) .strip() - .lstrip("[") - .rstrip("]") - .split(" ") + .lstrip("[") + .rstrip("]") + .split(" ") ] if AUTHZ_STANDARD_KEY in fi - and fi[AUTHZ_STANDARD_KEY] != "[]" + and fi[AUTHZ_STANDARD_KEY] != "[]" and fi[AUTHZ_STANDARD_KEY] else [] ) if ACL_STANDARD_KEY in fi: - if fi[ACL_STANDARD_KEY].strip().lower() in {"[u'open']", "['open']"}: - acl = ["*"] + if fi[ACL_STANDARD_KEY].strip().lower() in {"[u'open']", "['open']"}: + acl = ["*"] else: acl = ( [ element.strip() - .replace("'", "") - .replace('"', "") - .replace("%20", " ") + .replace("'", "") + .replace('"', "") + .replace("%20", " ") for element in _standardize_str(fi[ACL_STANDARD_KEY]) .strip() - .lstrip("[") - .rstrip("]") - .split(" ") + .lstrip("[") + .rstrip("]") + .split(" ") ] if ACL_STANDARD_KEY in fi - and fi[ACL_STANDARD_KEY] != "[]" + and fi[ACL_STANDARD_KEY] != "[]" and fi[ACL_STANDARD_KEY] else [] ) @@ -226,7 +226,7 @@

    Source code for gen3.tools.indexing.index_manifest

    if FILENAME_STANDARD_KEY in fi: file_name = _standardize_str(fi[FILENAME_STANDARD_KEY]) else: - file_name = "" + file_name = "" if fi.get(PREV_GUID_STANDARD_KEY): prev_guid = fi[PREV_GUID_STANDARD_KEY] @@ -243,7 +243,7 @@

    Source code for gen3.tools.indexing.index_manifest

    MD5_STANDARD_KEY ) != fi.get(MD5_STANDARD_KEY): logging.error( - "The guid {} with different size/hash already exists. Can not index it without getting a new guid".format( + "The guid {} with different size/hash already exists. Can not index it without getting a new guid".format( fi.get(GUID_STANDARD_KEY) ) ) @@ -254,7 +254,7 @@

    Source code for gen3.tools.indexing.index_manifest

    doc.urls = urls need_update = True - # indexd doesn't like when records have metadata for non-existing + # indexd doesn't like when records have metadata for non-existing # urls new_urls_metadata = copy.deepcopy(doc.urls_metadata) for url, metadata in doc.urls_metadata.items(): @@ -281,17 +281,17 @@

    Source code for gen3.tools.indexing.index_manifest

    need_update = True if need_update: - logging.info(f"updating {doc.did} to: {doc.to_json()}") + logging.info(f"updating {doc.did} to: {doc.to_json()}") doc.patch() else: if fi.get(GUID_STANDARD_KEY): - guid = fi.get(GUID_STANDARD_KEY, "").strip() + guid = fi.get(GUID_STANDARD_KEY, "").strip() else: guid = None record = { - "did": guid, - "hashes": {MD5_STANDARD_KEY: fi.get(MD5_STANDARD_KEY, "").strip()}, + "did": guid, + "hashes": {MD5_STANDARD_KEY: fi.get(MD5_STANDARD_KEY, "").strip()}, SIZE_STANDARD_KEY: fi.get(SIZE_STANDARD_KEY, 0), ACL_STANDARD_KEY: acl, AUTHZ_STANDARD_KEY: authz, @@ -300,41 +300,41 @@

    Source code for gen3.tools.indexing.index_manifest

    } if prev_guid: - logging.info(f"creating new version of {prev_guid}: {record}") + logging.info(f"creating new version of {prev_guid}: {record}") - # indexd exports a "form" field that gets populated in indexclient.create, + # indexd exports a "form" field that gets populated in indexclient.create, # but not indexclient.add_version, need to add manually here - record.update({"form": "object"}) + record.update({"form": "object"}) # to generate new GUID, new version indexd API expects body to not - # contain "did", rather than have it be None or "" - new_guid = record["did"] - if not record["did"]: - del record["did"] + # contain "did", rather than have it be None or "" + new_guid = record["did"] + if not record["did"]: + del record["did"] new_guid = None new_doc = Document(client=None, did=new_guid, json=record) # TODO: in the case where a new GUID *is* provided AND a version with that - # guid already exists AND you run this, it's gonna throw an error. + # guid already exists AND you run this, it's gonna throw an error. # We need to gracefully handle the error from indexd. there will # be a conflict where a version with this GUID already exists... # but if we can verify that the version has the correct values # for all the fields, we can effectively ignore this error and continue doc = indexclient.add_version(current_did=prev_guid, new_doc=new_doc) else: - logging.info(f"creating: {record}") + logging.info(f"creating: {record}") doc = indexclient.create(**record) fi[GUID_STANDARD_KEY] = doc.did except Exception as e: - # Don't break for any reason + # Don't break for any reason index_success = False exc_info = sys.exc_info() traceback.print_exception(*exc_info) logging.error( - "Can not update/create an indexd record with guid {}. Detail: {}".format( + "Can not update/create an indexd record with guid {}. Detail: {}".format( fi.get(GUID_STANDARD_KEY), e ) ) @@ -344,7 +344,7 @@

    Source code for gen3.tools.indexing.index_manifest

    try: if not mds: raise Exception( - "Can not submit to the metadata service when using indexd basic auth" + "Can not submit to the metadata service when using indexd basic auth" ) metadata = mds._prepare_metadata( fi, @@ -354,11 +354,11 @@

    Source code for gen3.tools.indexing.index_manifest

    if metadata: mds.create(guid=doc.did, metadata=metadata, overwrite=True) except Exception as e: - # Don't break, but delete indexd record + # Don't break, but delete indexd record exc_info = sys.exc_info() traceback.print_exception(*exc_info) logging.error( - "Can not create package metadata for guid {}. Deleting indexd record. Detail: {}".format( + "Can not create package metadata for guid {}. Deleting indexd record. Detail: {}".format( fi[GUID_STANDARD_KEY], e ) ) @@ -368,7 +368,7 @@

    Source code for gen3.tools.indexing.index_manifest

    exc_info = sys.exc_info() traceback.print_exception(*exc_info) logging.error( - "Cannot delete indexd record with {}. Detail: {}".format( + "Cannot delete indexd record with {}. Detail: {}".format( fi[GUID_STANDARD_KEY], e ) ) @@ -377,7 +377,7 @@

    Source code for gen3.tools.indexing.index_manifest

    thread_control.num_processed_files += 1 if (thread_control.num_processed_files * 10) % thread_control.num_total_files == 0: logging.info( - "Progress: {}%".format( + "Progress: {}%".format( thread_control.num_processed_files * 100.0 / thread_control.num_total_files @@ -395,11 +395,11 @@

    Source code for gen3.tools.indexing.index_manifest

    auth=None, replace_urls=True, manifest_file_delimiter=None, - output_filename="indexing-output-manifest.csv", + output_filename="indexing-output-manifest.csv", submit_additional_metadata_columns=False, force_metadata_columns_even_if_empty=True, ): - """ + """ Loop through all the files in the manifest, update/create records in indexd update indexd if the url is not in the record url list or acl has changed @@ -409,7 +409,7 @@

    Source code for gen3.tools.indexing.index_manifest

    thread_num(int): number of threads for indexing auth(Gen3Auth): Gen3 auth or tuple with basic auth name and password replace_urls(bool): flag to indicate if replace urls or not - manifest_file_delimiter(str): manifest's delimiter + manifest_file_delimiter(str): manifest's delimiter output_filename(str): output file name for manifest submit_additional_metadata_columns(bool): whether to submit additional metadata to the metadata service force_metadata_columns_even_if_empty(bool): force the creation of a metadata column @@ -423,58 +423,58 @@

    Source code for gen3.tools.indexing.index_manifest

    2, ..., , dataB, Resulting metadata if force_metadata_columns_even_if_empty=True : - "1": { - "columnA": "dataA", - "columnB": "", - "ColumnC": "", + "1": { + "columnA": "dataA", + "columnB": "", + "ColumnC": "", }, - "2": { - "columnA": "", - "columnB": "dataB", - "ColumnC": "", + "2": { + "columnA": "", + "columnB": "dataB", + "ColumnC": "", }, Resulting metadata if force_metadata_columns_even_if_empty=False : - "1": { - "columnA": "dataA", + "1": { + "columnA": "dataA", }, - "2": { - "columnB": "dataB", + "2": { + "columnB": "dataB", }, Returns: files(list(dict)): list of file info [ { - "guid": "guid_example", - "filename": "example", - "size": 100, - "acl": "['open']", - "md5": "md5_hash", + "guid": "guid_example", + "filename": "example", + "size": 100, + "acl": "['open']", + "md5": "md5_hash", }, ] headers(list(str)): list of fieldnames - """ - logging.info("Start the process ...") - service_location = "index" - commons_url = commons_url.strip("/") + """ + logging.info("Start the process ...") + service_location = "index" + commons_url = commons_url.strip("/") # if running locally, indexd is deployed by itself without a location relative # to the commons - if "http://localhost" in commons_url: - service_location = "" + if "http://localhost" in commons_url: + service_location = "" if not commons_url.endswith(service_location): - commons_url += "/" + service_location + commons_url += "/" + service_location - logging.info("\nUsing URL {}\n".format(commons_url)) + logging.info("\nUsing URL {}\n".format(commons_url)) - indexclient = client.IndexClient(commons_url, "v0", auth=auth) + indexclient = client.IndexClient(commons_url, "v0", auth=auth) if isinstance(auth, tuple): # basic auth if submit_additional_metadata_columns: logging.warning( - f"'submit_additional_metadata_columns' is on, but using indexd basic auth. Will not be able to submit to the metadata service. To create metadata, use Gen3Auth instance instead." + f"'submit_additional_metadata_columns' is on, but using indexd basic auth. Will not be able to submit to the metadata service. To create metadata, use Gen3Auth instance instead." ) mds = None else: # Gen3Auth @@ -487,7 +487,7 @@

    Source code for gen3.tools.indexing.index_manifest

    except Exception as e: exc_info = sys.exc_info() traceback.print_exception(*exc_info) - logging.error("Can not read {}. Detail: {}".format(manifest_file, e)) + logging.error("Can not read {}. Detail: {}".format(manifest_file, e)) return None, None # Early terminate @@ -522,7 +522,7 @@

    Source code for gen3.tools.indexing.index_manifest

    pool.join() output_filename = os.path.abspath(output_filename) - logging.info(f"Writing output to {output_filename}") + logging.info(f"Writing output to {output_filename}") # remove existing output if it exists if os.path.isfile(output_filename): @@ -536,42 +536,42 @@

    Source code for gen3.tools.indexing.index_manifest

    @click.command() @click.option( - "--commons-url", - "commons_url", - help="Root domain (url) for a commons containing indexd.", + "--commons-url", + "commons_url", + help="Root domain (url) for a commons containing indexd.", required=True, ) -@click.option("--manifest_file", help="The path to input manifest") +@click.option("--manifest_file", help="The path to input manifest") @click.option( - "--thread-num", - "thread_num", + "--thread-num", + "thread_num", type=int, - help="Number of threads", + help="Number of threads", default=1, show_default=True, ) -@click.option("--api-key", "api_key", help="path to api key") -@click.option("--auth", help="basic auth") +@click.option("--api-key", "api_key", help="path to api key") +@click.option("--auth", help="basic auth") @click.option( - "--replace-urls", - "replace_urls", + "--replace-urls", + "replace_urls", type=bool, - help="If supplied, will replace urls for existing records. e.g. existing urls will be overwritten by the new ones", + help="If supplied, will replace urls for existing records. e.g. existing urls will be overwritten by the new ones", default=False, show_default=True, ) @click.option( - "--manifest-file-delimiter", - "manifest_file_delimiter", - help="string character that delimites the file (tab or comma). Defaults to tab.", - default="\t", + "--manifest-file-delimiter", + "manifest_file_delimiter", + help="string character that delimites the file (tab or comma). Defaults to tab.", + default="\t", show_default=True, ) @click.option( - "--out-manifest-file", - "out_manifest_file", - help="The path to output manifest", - default="indexing-output-manifest.csv", + "--out-manifest-file", + "out_manifest_file", + help="The path to output manifest", + default="indexing-output-manifest.csv", show_default=True, ) def index_object_manifest_cli( @@ -584,7 +584,7 @@

    Source code for gen3.tools.indexing.index_manifest

    manifest_file_delimiter, out_manifest_file, ): - """ + """ Commandline interface for indexing a given manifest to indexd Args: @@ -596,18 +596,18 @@

    Source code for gen3.tools.indexing.index_manifest

    replace_urls(bool): Replace urls or not NOTE: if both api_key and auth are specified, it will ignore the later and take the former as a default - manifest_file_delimiter(str): manifest's delimiter + manifest_file_delimiter(str): manifest's delimiter out_manifest_file(str): path to the output manifest - """ + """ if api_key: auth = Gen3Auth(commons_url, refresh_file=api_key) else: - auth = tuple(auth.split(",")) if auth else None + auth = tuple(auth.split(",")) if auth else None files, headers = index_object_manifest( - commons_url + "/index", + commons_url + "/index", manifest_file, int(thread_num), auth, @@ -617,8 +617,8 @@

    Source code for gen3.tools.indexing.index_manifest

    ) -if __name__ == "__main__": - logging.basicConfig(filename="index_object_manifest.log", level=logging.DEBUG) +if __name__ == "__main__": + logging.basicConfig(filename="index_object_manifest.log", level=logging.DEBUG) logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) index_object_manifest_cli() @@ -626,47 +626,47 @@

    Source code for gen3.tools.indexing.index_manifest

    [docs] def delete_all_guids(auth, file): - """ + """ Delete all GUIDs specified in the object manifest. WARNING: THIS COMPLETELY REMOVES INDEX RECORDS. USE THIS ONLY IF YOU KNOW THE IMPLICATIONS. - """ + """ index = Gen3Index(auth.endpoint, auth_provider=auth) if not index.is_healthy(): logging.debug( - f"uh oh! The indexing service is not healthy in the commons {auth.endpoint}" + f"uh oh! The indexing service is not healthy in the commons {auth.endpoint}" ) exit() # try to get delimeter based on file ext file_ext = os.path.splitext(file) - if file_ext[-1].lower() == ".tsv": - manifest_file_delimiter = "\t" + if file_ext[-1].lower() == ".tsv": + manifest_file_delimiter = "\t" else: # default, assume CSV - manifest_file_delimiter = "," + manifest_file_delimiter = "," - with open(file, "r", encoding="utf-8-sig") as input_file: + with open(file, "r", encoding="utf-8-sig") as input_file: csvReader = csv.DictReader(input_file, delimiter=manifest_file_delimiter) fieldnames = csvReader.fieldnames - logging.debug(f"got fieldnames from {file}: {fieldnames}") + logging.debug(f"got fieldnames from {file}: {fieldnames}") # figure out which permutation of the name GUID is being used 1 time # then use it for all future rows - guid_name = "guid" - for name in ["guid", "GUID", "did", "DID"]: + guid_name = "guid" + for name in ["guid", "GUID", "did", "DID"]: if name in fieldnames: guid_name = name - logging.debug(f"using {guid_name} to retrieve GUID to delete...") + logging.debug(f"using {guid_name} to retrieve GUID to delete...") for row in csvReader: guid = row.get(guid_name) if guid: - logging.debug(f"deleting GUID record:{guid}") + logging.debug(f"deleting GUID record:{guid}") logging.debug(index.delete_record(guid=guid))
    diff --git a/docs/_build/html/_modules/gen3/tools/indexing/verify_manifest.html b/docs/_build/html/_modules/gen3/tools/indexing/verify_manifest.html index 5d9a9974a..c5f59de5f 100644 --- a/docs/_build/html/_modules/gen3/tools/indexing/verify_manifest.html +++ b/docs/_build/html/_modules/gen3/tools/indexing/verify_manifest.html @@ -31,10 +31,10 @@

    Source code for gen3.tools.indexing.verify_manifest

    -"""
    +"""
     Module for indexing actions for verifying a manifest of
    -indexed file objects (against indexd's API). Supports
    -multiple processes and coroutines using Python's asyncio library.
    +indexed file objects (against indexd's API). Supports
    +multiple processes and coroutines using Python's asyncio library.
     
     The default manifest format created is a Comma-Separated Value file (csv)
     with rows for every record. A header row is created with field names:
    @@ -53,10 +53,10 @@ 

    Source code for gen3.tools.indexing.verify_manifest

    from gen3.tools.indexing.verify_manifest import manifest_row_parsers def _get_authz_from_row(row): - return [row.get("authz").strip().strip("[").strip("]").strip("'")] + return [row.get("authz").strip().strip("[").strip("]").strip("'")] # override default parsers -manifest_row_parsers["authz"] = _get_authz_from_row +manifest_row_parsers["authz"] = _get_authz_from_row indexing.verify_object_manifest(COMMONS) ``` @@ -65,13 +65,13 @@

    Source code for gen3.tools.indexing.verify_manifest

    format: {guid}|{error_name}|expected {value_from_manifest}|actual {value_from_indexd} -ex: 93d9af72-b0f1-450c-a5c6-7d3d8d2083b4|authz|expected ['']|actual ['/programs/DEV/projects/test'] +ex: 93d9af72-b0f1-450c-a5c6-7d3d8d2083b4|authz|expected ['']|actual ['/programs/DEV/projects/test'] Attributes: CURRENT_DIR (str): directory this file is in MAX_CONCURRENT_REQUESTS (int): maximum number of desired concurrent requests across processes/threads -""" +""" import aiohttp import asyncio import csv @@ -90,7 +90,7 @@

    Source code for gen3.tools.indexing.verify_manifest

    def _get_guid_from_row(row): - """ + """ Given a row from the manifest, return the field representing expected indexd guid. Args: @@ -98,69 +98,69 @@

    Source code for gen3.tools.indexing.verify_manifest

    Returns: str: guid - """ - guid = row.get("guid") + """ + guid = row.get("guid") if not guid: - guid = row.get("GUID") + guid = row.get("GUID") return guid def _get_md5_from_row(row): - """ - Given a row from the manifest, return the field representing file's md5 sum. + """ + Given a row from the manifest, return the field representing file's md5 sum. Args: row (dict): column_name:row_value Returns: str: md5 sum for file - """ - if "md5" in row: - return row["md5"] - elif "md5sum" in row: - return row["md5sum"] + """ + if "md5" in row: + return row["md5"] + elif "md5sum" in row: + return row["md5sum"] else: return None def _get_file_size_from_row(row): - """ - Given a row from the manifest, return the field representing file's size in bytes. + """ + Given a row from the manifest, return the field representing file's size in bytes. Args: row (dict): column_name:row_value Returns: int: integer representing file size in bytes - """ + """ try: - if "file_size" in row: - return int(row["file_size"]) - elif "size" in row: - return int(row["size"]) + if "file_size" in row: + return int(row["file_size"]) + elif "size" in row: + return int(row["size"]) else: return None except Exception: - logging.warning(f"could not convert this to an int: {row.get('file_size')}") - return row.get("file_size") + logging.warning(f"could not convert this to an int: {row.get('file_size')}") + return row.get("file_size") def _get_acl_from_row(row): - """ - Given a row from the manifest, return the field representing file's expected acls. + """ + Given a row from the manifest, return the field representing file's expected acls. Args: row (dict): column_name:row_value Returns: List[str]: acls for the indexd record - """ - return [item for item in row.get("acl", "").strip().split(" ") if item] + """ + return [item for item in row.get("acl", "").strip().split(" ") if item] def _get_authz_from_row(row): - """ - Given a row from the manifest, return the field representing file's expected authz + """ + Given a row from the manifest, return the field representing file's expected authz resources. Args: @@ -168,56 +168,56 @@

    Source code for gen3.tools.indexing.verify_manifest

    Returns: List[str]: authz resources for the indexd record - """ - return [item for item in row.get("authz", "").strip().split(" ") if item] + """ + return [item for item in row.get("authz", "").strip().split(" ") if item] def _get_urls_from_row(row): - """ - Given a row from the manifest, return the field representing file's expected urls. + """ + Given a row from the manifest, return the field representing file's expected urls. Args: row (dict): column_name:row_value Returns: List[str]: urls for indexd record file location(s) - """ - if "urls" in row: - return [item for item in row.get("urls", "").strip().split(" ") if item] - elif "url" in row: - return [item for item in row.get("urls", "").strip().split(" ") if item] + """ + if "urls" in row: + return [item for item in row.get("urls", "").strip().split(" ") if item] + elif "url" in row: + return [item for item in row.get("urls", "").strip().split(" ") if item] else: return [] def _get_file_name_from_row(row): - """ - Given a row from the manifest, return the field representing file's expected file_name. + """ + Given a row from the manifest, return the field representing file's expected file_name. Args: row (dict): column_name:row_value Returns: List[str]: file_name for indexd record file location(s) - """ - if "file_name" in row: - return row["file_name"] - elif "filename" in row: - return row["filename"] - elif "name" in row: - return row["name"] + """ + if "file_name" in row: + return row["file_name"] + elif "filename" in row: + return row["filename"] + elif "name" in row: + return row["name"] else: return None manifest_row_parsers = { - "guid": _get_guid_from_row, - "md5": _get_md5_from_row, - "file_size": _get_file_size_from_row, - "acl": _get_acl_from_row, - "authz": _get_authz_from_row, - "urls": _get_urls_from_row, - "file_name": _get_file_name_from_row, + "guid": _get_guid_from_row, + "md5": _get_md5_from_row, + "file_size": _get_file_size_from_row, + "acl": _get_acl_from_row, + "authz": _get_authz_from_row, + "urls": _get_urls_from_row, + "file_name": _get_file_name_from_row, } @@ -231,7 +231,7 @@

    Source code for gen3.tools.indexing.verify_manifest

    manifest_file_delimiter=None, output_filename=None, ): - """ + """ Verify all file object records into a manifest csv Args: @@ -241,23 +241,23 @@

    Source code for gen3.tools.indexing.verify_manifest

    manifest_row_parsers (Dict{indexd_field:func_to_parse_row}): Row parsers manifest_file_delimiter (str): delimeter in manifest_file output_filename (str): filename for output logs - """ + """ if not output_filename: - output_filename = f"verify-manifest-errors-{time.time()}.log" + output_filename = f"verify-manifest-errors-{time.time()}.log" start_time = time.perf_counter() - logging.info(f"start time: {start_time}") + logging.info(f"start time: {start_time}") # if delimiter not specified, try to get based on file ext if not manifest_file_delimiter: file_ext = os.path.splitext(manifest_file) - if file_ext[-1].lower() == ".tsv": - manifest_file_delimiter = "\t" + if file_ext[-1].lower() == ".tsv": + manifest_file_delimiter = "\t" else: # default, assume CSV - manifest_file_delimiter = "," + manifest_file_delimiter = "," - logging.debug(f"detected {manifest_file_delimiter} as delimiter between columns") + logging.debug(f"detected {manifest_file_delimiter} as delimiter between columns") await _verify_all_index_records_in_file( commons_url, @@ -268,8 +268,8 @@

    Source code for gen3.tools.indexing.verify_manifest

    ) end_time = time.perf_counter() - logging.info(f"end time: {end_time}") - logging.info(f"run time: {end_time-start_time}")
    + logging.info(f"end time: {end_time}") + logging.info(f"run time: {end_time-start_time}")
    @@ -280,14 +280,14 @@

    Source code for gen3.tools.indexing.verify_manifest

    max_concurrent_requests, output_filename, ): - """ + """ Getting indexd records and writing to a file. This function creates semaphores to limit the number of concurrent http connections that get opened to send requests to indexd. It then uses asyncio to start a number of coroutines. Steps: 1) requests to indexd to get records (writes resulting records to a queue) - 2) puts a final "DONE" in the queue to stop coroutine that will read from queue + 2) puts a final "DONE" in the queue to stop coroutine that will read from queue 3) reading those records from the queue and writing to a file Args: @@ -296,14 +296,14 @@

    Source code for gen3.tools.indexing.verify_manifest

    manifest_file_delimiter (str): delimeter in manifest_file output_filename (str, optional): filename for output max_concurrent_requests (int): the maximum number of concurrent requests allowed - """ + """ max_requests = int(max_concurrent_requests) - logging.debug(f"max concurrent requests: {max_requests}") + logging.debug(f"max concurrent requests: {max_requests}") lock = asyncio.Semaphore(max_requests) queue = asyncio.Queue() output_queue = asyncio.Queue() - with open(manifest_file, encoding="utf-8-sig") as manifest: + with open(manifest_file, encoding="utf-8-sig") as manifest: reader = csv.DictReader(manifest, delimiter=manifest_file_delimiter) for row in reader: @@ -316,16 +316,16 @@

    Source code for gen3.tools.indexing.verify_manifest

    await queue.put(new_row) for _ in range(0, int(max_concurrent_requests + (max_concurrent_requests / 4))): - await queue.put("DONE") + await queue.put("DONE") await asyncio.gather( *( _parse_from_queue(queue, lock, commons_url, output_queue) - # why "+ (max_concurrent_requests / 4)"? + # why "+ (max_concurrent_requests / 4)"? # This is because the max requests at any given time could be - # waiting for metadata responses all at once and there's processing done + # waiting for metadata responses all at once and there's processing done # before that semaphore, so this just adds a few extra processes to get - # through the queue up to that point of metadata requests so it's ready + # through the queue up to that point of metadata requests so it's ready # right away when a lock is released. Not entirely necessary but speeds # things up a tiny bit to always ensure something is waiting for that lock for x in range( @@ -336,23 +336,23 @@

    Source code for gen3.tools.indexing.verify_manifest

    output_filename = os.path.abspath(output_filename) logging.info( - f"done processing, writing output queue to single file {output_filename}" + f"done processing, writing output queue to single file {output_filename}" ) # remove existing output if it exists if os.path.isfile(output_filename): os.unlink(output_filename) - with open(output_filename, "w") as outfile: + with open(output_filename, "w") as outfile: while not output_queue.empty(): line = await output_queue.get() outfile.write(line) - logging.info(f"done writing output to file {output_filename}") + logging.info(f"done writing output to file {output_filename}") async def _parse_from_queue(queue, lock, commons_url, output_queue): - """ + """ Keep getting items from the queue and verifying that indexd contains the expected fields from that row. If there are any issues, log errors into a file. Return when nothing is left in the queue. @@ -363,81 +363,81 @@

    Source code for gen3.tools.indexing.verify_manifest

    connections commons_url (str): root domain for commons where indexd lives output_queue (asyncio.Queue): queue for output - """ + """ loop = get_or_create_event_loop_for_thread() row = await queue.get() - while row != "DONE": - guid = manifest_row_parsers["guid"](row) - authz = manifest_row_parsers["authz"](row) - acl = manifest_row_parsers["acl"](row) - file_size = manifest_row_parsers["file_size"](row) - md5 = manifest_row_parsers["md5"](row) - urls = manifest_row_parsers["urls"](row) - file_name = manifest_row_parsers["file_name"](row) + while row != "DONE": + guid = manifest_row_parsers["guid"](row) + authz = manifest_row_parsers["authz"](row) + acl = manifest_row_parsers["acl"](row) + file_size = manifest_row_parsers["file_size"](row) + md5 = manifest_row_parsers["md5"](row) + urls = manifest_row_parsers["urls"](row) + file_name = manifest_row_parsers["file_name"](row) actual_record = await _get_record_from_indexd(guid, commons_url, lock) if not actual_record: - output = f"{guid}|no_record|expected {row}|actual None\n" + output = f"{guid}|no_record|expected {row}|actual None\n" await output_queue.put(output) logging.error(output) else: - logging.info(f"verifying {guid}...") + logging.info(f"verifying {guid}...") - if sorted(authz) != sorted(actual_record["authz"]): + if sorted(authz) != sorted(actual_record["authz"]): output = ( - f"{guid}|authz|expected {authz}|actual {actual_record['authz']}\n" + f"{guid}|authz|expected {authz}|actual {actual_record['authz']}\n" ) await output_queue.put(output) logging.error(output) - if sorted(acl) != sorted(actual_record["acl"]): - output = f"{guid}|acl|expected {acl}|actual {actual_record['acl']}\n" + if sorted(acl) != sorted(actual_record["acl"]): + output = f"{guid}|acl|expected {acl}|actual {actual_record['acl']}\n" await output_queue.put(output) logging.error(output) - if file_size != actual_record["size"]: + if file_size != actual_record["size"]: if ( not file_size and file_size != 0 - and not actual_record["size"] - and actual_record["size"] != 0 + and not actual_record["size"] + and actual_record["size"] != 0 ): # actual and expected are both either empty string or None - # so even though they're not equal, they represent null value so - # we don't need to consider this an error in validation + # so even though they're not equal, they represent null value so + # we don't need to consider this an error in validation pass else: - output = f"{guid}|file_size|expected {file_size}|actual {actual_record['size']}\n" + output = f"{guid}|file_size|expected {file_size}|actual {actual_record['size']}\n" await output_queue.put(output) logging.error(output) - if md5 != actual_record["hashes"].get("md5"): + if md5 != actual_record["hashes"].get("md5"): if ( not md5 and md5 != 0 - and not actual_record["hashes"].get("md5") - and actual_record["hashes"].get("md5") != 0 + and not actual_record["hashes"].get("md5") + and actual_record["hashes"].get("md5") != 0 ): # actual and expected are both either empty string or None - # so even though they're not equal, they represent null value so - # we don't need to consider this an error in validation + # so even though they're not equal, they represent null value so + # we don't need to consider this an error in validation pass else: - output = f"{guid}|md5|expected {md5}|actual {actual_record['hashes'].get('md5')}\n" + output = f"{guid}|md5|expected {md5}|actual {actual_record['hashes'].get('md5')}\n" await output_queue.put(output) logging.error(output) - urls = [url.replace("%20", " ") for url in urls] - if sorted(urls) != sorted(actual_record["urls"]): - output = f"{guid}|urls|expected {urls}|actual {actual_record['urls']}\n" + urls = [url.replace("%20", " ") for url in urls] + if sorted(urls) != sorted(actual_record["urls"]): + output = f"{guid}|urls|expected {urls}|actual {actual_record['urls']}\n" await output_queue.put(output) logging.error(output) - if not actual_record["file_name"] and file_name: - # if the actual record name is "" or None but something was specified + if not actual_record["file_name"] and file_name: + # if the actual record name is "" or None but something was specified # in the manifest, we have a problem - output = f"{guid}|file_name|expected {file_name}|actual {actual_record['file_name']}\n" + output = f"{guid}|file_name|expected {file_name}|actual {actual_record['file_name']}\n" await output_queue.put(output) logging.error(output) @@ -445,7 +445,7 @@

    Source code for gen3.tools.indexing.verify_manifest

    async def _get_record_from_indexd(guid, commons_url, lock): - """ + """ Gets a semaphore then requests a record for the given guid Args: @@ -453,12 +453,12 @@

    Source code for gen3.tools.indexing.verify_manifest

    commons_url (str): root domain for commons where indexd lives lock (asyncio.Semaphore): semaphones used to limit ammount of concurrent http connections - """ + """ index = Gen3Index(commons_url) async with lock: - # default ssl handling unless it's explicitly http:// + # default ssl handling unless it's explicitly http:// ssl = None - if "https" not in commons_url: + if "https" not in commons_url: ssl = False record = None @@ -467,7 +467,7 @@

    Source code for gen3.tools.indexing.verify_manifest

    return await index.async_get_record(guid, _ssl=ssl) except aiohttp.client_exceptions.ClientResponseError as exc: - logging.warning(f"couldn't get record. error: {exc}") + logging.warning(f"couldn't get record. error: {exc}") return record
    diff --git a/docs/_build/html/_modules/gen3/tools/metadata/ingest_manifest.html b/docs/_build/html/_modules/gen3/tools/metadata/ingest_manifest.html index 64446862e..830a16dd5 100644 --- a/docs/_build/html/_modules/gen3/tools/metadata/ingest_manifest.html +++ b/docs/_build/html/_modules/gen3/tools/metadata/ingest_manifest.html @@ -31,7 +31,7 @@

    Source code for gen3.tools.metadata.ingest_manifest

    -"""
    +"""
     Tools for ingesting a CSV/TSV metadata manifest into the Metdata Service.
     
     Attributes:
    @@ -42,16 +42,16 @@ 

    Source code for gen3.tools.metadata.ingest_manifest

    NOT exist in indexd manifest_row_parsers (Dict{str: function}): functions for parsing, users can override manifest_row_parsers = { - "guid_from_file": _get_guid_for_row, - "indexed_file_object_guid": _query_for_associated_indexd_record_guid, + "guid_from_file": _get_guid_for_row, + "indexed_file_object_guid": _query_for_associated_indexd_record_guid, } - "guid_for_row" is the function to retrieve the guid from the given file - "indexed_file_object_guid" is the function to retrieve the guid from elsewhere, + "guid_for_row" is the function to retrieve the guid from the given file + "indexed_file_object_guid" is the function to retrieve the guid from elsewhere, like indexd (by querying) MAX_CONCURRENT_REQUESTS (int): Maximum concurrent requests to mds for ingestion -""" +""" import aiohttp import asyncio import csv @@ -65,19 +65,19 @@

    Source code for gen3.tools.metadata.ingest_manifest

    from gen3.index import Gen3Index from gen3.metadata import Gen3Metadata -TMP_FOLDER = os.path.abspath("./tmp") + "/" +TMP_FOLDER = os.path.abspath("./tmp") + "/" CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) MAX_CONCURRENT_REQUESTS = 24 -COLUMN_TO_USE_AS_GUID = "guid" -GUID_TYPE_FOR_INDEXED_FILE_OBJECT = "indexed_file_object" -GUID_TYPE_FOR_NON_INDEXED_FILE_OBJECT = "metadata_object" +COLUMN_TO_USE_AS_GUID = "guid" +GUID_TYPE_FOR_INDEXED_FILE_OBJECT = "indexed_file_object" +GUID_TYPE_FOR_NON_INDEXED_FILE_OBJECT = "metadata_object" logging = get_logger(__name__) def _get_guid_for_row(commons_url, row, lock): - """ + """ Given a row from the manifest, return the guid to use for the metadata object. Args: @@ -88,21 +88,21 @@

    Source code for gen3.tools.metadata.ingest_manifest

    Returns: str: guid - """ + """ return row.get(COLUMN_TO_USE_AS_GUID) async def _query_for_associated_indexd_record_guid( commons_url, row, lock, output_queue ): - """ + """ Given a row from the manifest, return the guid for the related indexd record. - By default attempts to use a column "submitted_sample_id" to pattern match + By default attempts to use a column "submitted_sample_id" to pattern match URLs in indexd records to find a single match. For example: - "NWD12345" would match a record with url: "s3://some-bucket/file_NWD12345.cram" + "NWD12345" would match a record with url: "s3://some-bucket/file_NWD12345.cram" WARNING: The query endpoint this uses in indexd is incredibly slow when there are lots of indexd records. @@ -116,21 +116,21 @@

    Source code for gen3.tools.metadata.ingest_manifest

    Returns: str: guid or None - """ - mapping = {"urls": "submitted_sample_id"} + """ + mapping = {"urls": "submitted_sample_id"} # Alternate example: # # mapping = { - # "acl": "study_with_consent", - # "size": "file_size", + # "acl": "study_with_consent", + # "size": "file_size", # } - if "urls" in mapping and len(mapping.items()) > 1: + if "urls" in mapping and len(mapping.items()) > 1: msg = ( - "You cannot pattern match 'urls' and exact match other fields for mapping " - "from indexd record to metadata columns. You can match by URL pattern " - "*OR* match exact record fields like size, hash, uploader, url, acl, authz." - f"\nYou provided mapping: {mapping}" + "You cannot pattern match 'urls' and exact match other fields for mapping " + "from indexd record to metadata columns. You can match by URL pattern " + "*OR* match exact record fields like size, hash, uploader, url, acl, authz." + f"\nYou provided mapping: {mapping}" ) logging.error(msg) await output_queue.put(msg) @@ -138,25 +138,25 @@

    Source code for gen3.tools.metadata.ingest_manifest

    # special query endpoint for matching url patterns, other fields # just use get with params - if "urls" in mapping: - pattern = row.get(mapping["urls"]) - logging.debug(f"trying to find matching record matching url pattern: {pattern}") + if "urls" in mapping: + pattern = row.get(mapping["urls"]) + logging.debug(f"trying to find matching record matching url pattern: {pattern}") records = await async_query_urls_from_indexd(pattern, commons_url, lock) else: params = { mapping_key: row.get(mapping_value) for mapping_key, mapping_value in mapping.items() } - logging.debug(f"trying to find matching record matching params: {params}") + logging.debug(f"trying to find matching record matching params: {params}") records = await _get_with_params_from_indexd(params, commons_url, lock) - logging.debug(f"matching record(s): {records}") + logging.debug(f"matching record(s): {records}") if len(records) > 1: msg = ( - "Multiple records were found with the given search criteria, this is assumed " - "to be unintentional so the metadata will NOT be linked to these records:\n" - f"{records}" + "Multiple records were found with the given search criteria, this is assumed " + "to be unintentional so the metadata will NOT be linked to these records:\n" + f"{records}" ) logging.warning(msg) await output_queue.put(msg) @@ -164,14 +164,14 @@

    Source code for gen3.tools.metadata.ingest_manifest

    guid = None if len(records) == 1: - guid = records[0].get("did") + guid = records[0].get("did") return guid manifest_row_parsers = { - "guid_for_row": _get_guid_for_row, - "indexed_file_object_guid": _query_for_associated_indexd_record_guid, + "guid_for_row": _get_guid_for_row, + "indexed_file_object_guid": _query_for_associated_indexd_record_guid, } @@ -189,7 +189,7 @@

    Source code for gen3.tools.metadata.ingest_manifest

    get_guid_from_file=True, metadata_type=None, ): - """ + """ Ingest all metadata records into a manifest csv Args: @@ -204,24 +204,24 @@

    Source code for gen3.tools.metadata.ingest_manifest

    output_filename (str): filename for output logs get_guid_from_file (bool): whether or not to get the guid for metadata from file NOTE: When this is True, will use the function in - manifest_row_parsers["guid_for_row"] to determine the GUID - (usually just a specific column in the file row like "guid") + manifest_row_parsers["guid_for_row"] to determine the GUID + (usually just a specific column in the file row like "guid") metadata_type (str): the type of metadata to be filled into the _guid_type field. If provided, will override the default logic per GUID: (GUID_TYPE_FOR_INDEXED_FILE_OBJECT if is_indexed_file_object else GUID_TYPE_FOR_NON_INDEXED_FILE_OBJECT) - """ + """ if not output_filename: - output_filename = f"ingest-metadata-manifest-errors-{time.time()}.log" + output_filename = f"ingest-metadata-manifest-errors-{time.time()}.log" # if delimiter not specified, try to get based on file ext if not manifest_file_delimiter: file_ext = os.path.splitext(manifest_file) - if file_ext[-1].lower() == ".tsv": - manifest_file_delimiter = "\t" + if file_ext[-1].lower() == ".tsv": + manifest_file_delimiter = "\t" else: # default, assume CSV - manifest_file_delimiter = "," + manifest_file_delimiter = "," await _ingest_all_metadata_in_file( commons_url, @@ -230,7 +230,7 @@

    Source code for gen3.tools.metadata.ingest_manifest

    auth, manifest_file_delimiter, max_concurrent_requests, - output_filename.split("/")[-1], + output_filename.split("/")[-1], get_guid_from_file, metadata_type, )
    @@ -248,7 +248,7 @@

    Source code for gen3.tools.metadata.ingest_manifest

    get_guid_from_file, metadata_type, ): - """ + """ Ingest metadata from file into metadata service. This function creates semaphores to limit the number of concurrent http connections that get opened to send requests to mds. @@ -268,36 +268,36 @@

    Source code for gen3.tools.metadata.ingest_manifest

    output_filename (str): filename for output logs get_guid_from_file (bool): whether or not to get the guid for metadata from file NOTE: When this is True, will use the function in - manifest_row_parsers["guid_for_row"] to determine the GUID - (usually just a specific column in the file row like "guid") + manifest_row_parsers["guid_for_row"] to determine the GUID + (usually just a specific column in the file row like "guid") metadata_type (str): the type of metadata to be filled into the _guid_type field. If provided, will override the default logic per GUID: (GUID_TYPE_FOR_INDEXED_FILE_OBJECT if is_indexed_file_object else GUID_TYPE_FOR_NON_INDEXED_FILE_OBJECT) - """ + """ max_requests = int(max_concurrent_requests) - logging.debug(f"max concurrent requests: {max_requests}") + logging.debug(f"max concurrent requests: {max_requests}") lock = asyncio.Semaphore(max_requests) queue = asyncio.Queue() output_queue = asyncio.Queue() start_time = time.perf_counter() - msg = f"start time: {start_time}" + msg = f"start time: {start_time}" logging.info(msg) await output_queue.put(msg) - with open(manifest_file, encoding="utf-8-sig") as manifest: + with open(manifest_file, encoding="utf-8-sig") as manifest: reader = csv.DictReader(manifest, delimiter=manifest_file_delimiter) for row in reader: new_row = {} for key, value in row.items(): # I know this looks crazy, DictReader is doing goofy things when - # column contains a JSON-like string so we're trying to fix it here + # column contains a JSON-like string so we're trying to fix it here # Basically make sure the resulting column is something that we can # later json.loads(). # remove redudant quoting if value: - value = value.strip().strip("'").strip('"').replace("''", "'") + value = value.strip().strip("'").strip('"').replace("''", "'") new_row[key.strip()] = value await queue.put(new_row) @@ -313,11 +313,11 @@

    Source code for gen3.tools.metadata.ingest_manifest

    metadata_source, metadata_type, ) - # why "+ (max_concurrent_requests / 4)"? + # why "+ (max_concurrent_requests / 4)"? # This is because the max requests at any given time could be - # waiting for metadata responses all at once and there's processing done + # waiting for metadata responses all at once and there's processing done # before that semaphore, so this just adds a few extra processes to get - # through the queue up to that point of metadata requests so it's ready + # through the queue up to that point of metadata requests so it's ready # right away when a lock is released. Not entirely necessary but speeds # things up a tiny bit to always ensure something is waiting for that lock for x in range( @@ -327,29 +327,29 @@

    Source code for gen3.tools.metadata.ingest_manifest

    ) end_time = time.perf_counter() - msg = f"end time: {end_time}" + msg = f"end time: {end_time}" logging.info(msg) await output_queue.put(msg) - msg = f"run time: {end_time-start_time}" + msg = f"run time: {end_time-start_time}" logging.info(msg) await output_queue.put(msg) output_filename = os.path.abspath(output_filename) logging.info( - f"done processing, writing output queue to single file {output_filename}" + f"done processing, writing output queue to single file {output_filename}" ) # remove existing output if it exists if os.path.isfile(output_filename): os.unlink(output_filename) - with open(output_filename, "w") as outfile: + with open(output_filename, "w") as outfile: while not output_queue.empty(): line = await output_queue.get() - outfile.write(line + "\n") + outfile.write(line + "\n") - logging.info(f"done writing output to file {output_filename}") + logging.info(f"done writing output to file {output_filename}") async def _parse_from_queue( @@ -362,7 +362,7 @@

    Source code for gen3.tools.metadata.ingest_manifest

    metadata_source, metadata_type, ): - """ + """ Keep getting items from the queue and checking if indexd contains a record with that guid. Then create/update metadta for that GUID in the metadata service. Also log to output queue. Return when nothing is left in the queue. @@ -376,42 +376,42 @@

    Source code for gen3.tools.metadata.ingest_manifest

    auth (Gen3Auth): Gen3 auth or tuple with basic auth name and password get_guid_from_file (bool): whether or not to get the guid for metadata from file NOTE: When this is True, will use the function in - manifest_row_parsers["guid_for_row"] to determine the GUID - (usually just a specific column in the file row like "guid") + manifest_row_parsers["guid_for_row"] to determine the GUID + (usually just a specific column in the file row like "guid") metadata_source (str): the name of the source of metadata (used to namespace in the metadata service) ex: dbgap metadata_type (str): the type of metadata to be filled into the _guid_type field. If provided, will override the default logic per GUID: (GUID_TYPE_FOR_INDEXED_FILE_OBJECT if is_indexed_file_object else GUID_TYPE_FOR_NON_INDEXED_FILE_OBJECT) - """ + """ while not queue.empty(): row = await queue.get() if get_guid_from_file: - guid = manifest_row_parsers["guid_for_row"](commons_url, row, lock) + guid = manifest_row_parsers["guid_for_row"](commons_url, row, lock) is_indexed_file_object = await _is_indexed_file_object( guid, commons_url, lock ) else: - guid = await manifest_row_parsers["indexed_file_object_guid"]( + guid = await manifest_row_parsers["indexed_file_object_guid"]( commons_url, row, lock, output_queue ) is_indexed_file_object = True if guid: - # construct metadata from rows, don't include redundant guid column - logging.debug(f"row: {row}") + # construct metadata from rows, don't include redundant guid column + logging.debug(f"row: {row}") metadata_from_file = {} for key, value in row.items(): try: new_value = json.loads(value) except json.decoder.JSONDecodeError as exc: - if "}" in value or "{" in value or "[" in value or "]" in value: + if "}" in value or "{" in value or "[" in value or "]" in value: msg = ( - f"Unable to json.loads a string that looks like json: {value}. " - f"adding as a string instead of nested json. Exception: {exc}" + f"Unable to json.loads a string that looks like json: {value}. " + f"adding as a string instead of nested json. Exception: {exc}" ) logging.warning(msg) await output_queue.put(msg) @@ -422,47 +422,47 @@

    Source code for gen3.tools.metadata.ingest_manifest

    if COLUMN_TO_USE_AS_GUID in metadata_from_file.keys(): del metadata_from_file[COLUMN_TO_USE_AS_GUID] - logging.debug(f"metadata from file: {metadata_from_file}") + logging.debug(f"metadata from file: {metadata_from_file}") # namespace by metadata source metadata = {metadata_source: metadata_from_file} if metadata_type: - metadata["_guid_type"] = metadata_type + metadata["_guid_type"] = metadata_type else: - metadata["_guid_type"] = ( + metadata["_guid_type"] = ( GUID_TYPE_FOR_INDEXED_FILE_OBJECT if is_indexed_file_object else GUID_TYPE_FOR_NON_INDEXED_FILE_OBJECT ) - logging.debug(f"metadata: {metadata}") + logging.debug(f"metadata: {metadata}") try: await _create_metadata(guid, metadata, auth, commons_url, lock) - msg = f"Successfully created {guid}" + msg = f"Successfully created {guid}" logging.info(msg) await output_queue.put(msg) except Exception as exc: logging.debug( - f"Got conflict for {guid}. Let's update instead of create..." + f"Got conflict for {guid}. Let's update instead of create..." ) await _update_metadata(guid, metadata, auth, commons_url, lock) - msg = f"Successfully updated {guid}" + msg = f"Successfully updated {guid}" logging.info(msg) await output_queue.put(msg) else: msg = ( - f"Did not add a metadata object for row because an invalid " - f"GUID was parsed or no record with this GUID was found in " - f"indexd: {guid}.\nRow: {row}" + f"Did not add a metadata object for row because an invalid " + f"GUID was parsed or no record with this GUID was found in " + f"indexd: {guid}.\nRow: {row}" ) logging.warning(msg) await output_queue.put(msg) async def _create_metadata(guid, metadata, auth, commons_url, lock): - """ + """ Gets a semaphore then creates metadata for guid Args: @@ -472,12 +472,12 @@

    Source code for gen3.tools.metadata.ingest_manifest

    commons_url (str): root domain for commons where metadata service lives lock (asyncio.Semaphore): semaphones used to limit ammount of concurrent http connections - """ + """ mds = Gen3Metadata(commons_url, auth_provider=auth) async with lock: - # default ssl handling unless it's explicitly http:// + # default ssl handling unless it's explicitly http:// ssl = None - if "https" not in commons_url: + if "https" not in commons_url: ssl = False response = await mds.async_create(guid, metadata, _ssl=ssl) @@ -485,7 +485,7 @@

    Source code for gen3.tools.metadata.ingest_manifest

    async def _update_metadata(guid, metadata, auth, commons_url, lock): - """ + """ Gets a semaphore then updates metadata for guid Args: @@ -495,12 +495,12 @@

    Source code for gen3.tools.metadata.ingest_manifest

    commons_url (str): root domain for commons where metadata service lives lock (asyncio.Semaphore): semaphones used to limit ammount of concurrent http connections - """ + """ mds = Gen3Metadata(commons_url, auth_provider=auth) async with lock: - # default ssl handling unless it's explicitly http:// + # default ssl handling unless it's explicitly http:// ssl = None - if "https" not in commons_url: + if "https" not in commons_url: ssl = False response = await mds.async_update(guid, metadata, _ssl=ssl) @@ -508,7 +508,7 @@

    Source code for gen3.tools.metadata.ingest_manifest

    async def _is_indexed_file_object(guid, commons_url, lock): - """ + """ Gets a semaphore then requests a record for the given guid Args: @@ -516,12 +516,12 @@

    Source code for gen3.tools.metadata.ingest_manifest

    commons_url (str): root domain for commons where mds lives lock (asyncio.Semaphore): semaphones used to limit ammount of concurrent http connections - """ + """ index = Gen3Index(commons_url) async with lock: - # default ssl handling unless it's explicitly http:// + # default ssl handling unless it's explicitly http:// ssl = None - if "https" not in commons_url: + if "https" not in commons_url: ssl = False try: @@ -536,7 +536,7 @@

    Source code for gen3.tools.metadata.ingest_manifest

    [docs] async def async_query_urls_from_indexd(pattern, commons_url, lock): - """ + """ Gets a semaphore then requests a record for the given pattern Args: @@ -544,12 +544,12 @@

    Source code for gen3.tools.metadata.ingest_manifest

    commons_url (str): root domain for commons where mds lives lock (asyncio.Semaphore): semaphones used to limit ammount of concurrent http connections - """ + """ index = Gen3Index(commons_url) async with lock: - # default ssl handling unless it's explicitly http:// + # default ssl handling unless it's explicitly http:// ssl = None - if "https" not in commons_url: + if "https" not in commons_url: ssl = False return await index.async_query_urls(pattern, _ssl=ssl)
    @@ -557,7 +557,7 @@

    Source code for gen3.tools.metadata.ingest_manifest

    async def _get_with_params_from_indexd(params, commons_url, lock): - """ + """ Gets a semaphore then requests a record for the given params Args: @@ -565,12 +565,12 @@

    Source code for gen3.tools.metadata.ingest_manifest

    commons_url (str): root domain for commons where mds lives lock (asyncio.Semaphore): semaphones used to limit ammount of concurrent http connections - """ + """ index = Gen3Index(commons_url) async with lock: - # default ssl handling unless it's explicitly http:// + # default ssl handling unless it's explicitly http:// ssl = None - if "https" not in commons_url: + if "https" not in commons_url: ssl = False return await index.async_get_with_params(params, _ssl=ssl) diff --git a/docs/_build/html/_modules/gen3/wss.html b/docs/_build/html/_modules/gen3/wss.html index c39cfb4f2..393b1936e 100644 --- a/docs/_build/html/_modules/gen3/wss.html +++ b/docs/_build/html/_modules/gen3/wss.html @@ -49,32 +49,32 @@

    Source code for gen3.wss

     
     
     def wsurl_to_tokens(ws_urlstr):
    -    """Tokenize ws:/// paths - so ws:///@user/bla/foo returns ("@user", "bla/foo")"""
    +    """Tokenize ws:/// paths - so ws:///@user/bla/foo returns ("@user", "bla/foo")"""
         urlparts = urlparse(ws_urlstr)
    -    if urlparts.scheme != "ws":
    -        raise Exception("invalid path {}".format(ws_urlstr))
    -    pathparts = [part for part in urlparts.path.split("/") if part]
    +    if urlparts.scheme != "ws":
    +        raise Exception("invalid path {}".format(ws_urlstr))
    +    pathparts = [part for part in urlparts.path.split("/") if part]
         if len(pathparts) < 1:
    -        raise Exception("invalid path {}".format(ws_urlstr))
    -    return (pathparts[0], "/".join(pathparts[1:]))
    +        raise Exception("invalid path {}".format(ws_urlstr))
    +    return (pathparts[0], "/".join(pathparts[1:]))
     
     
     @backoff.on_exception(backoff.expo, requests.HTTPError, **DEFAULT_BACKOFF_SETTINGS)
     def get_url(urlstr, dest_path):
    -    """Simple url fetch to dest_path with backoff"""
    +    """Simple url fetch to dest_path with backoff"""
         res = requests.get(urlstr)
         raise_for_status_and_print_error(res)
    -    if dest_path == "-":
    +    if dest_path == "-":
             sys.stdout.write(res.text)
         else:
    -        with open(dest_path, "wb") as f:
    +        with open(dest_path, "wb") as f:
                 f.write(res.content)
     
     
     @backoff.on_exception(backoff.expo, requests.HTTPError, **DEFAULT_BACKOFF_SETTINGS)
     def put_url(urlstr, src_path):
    -    """Simple put src_path to url with backoff"""
    -    with open(src_path, "rb") as f:
    +    """Simple put src_path to url with backoff"""
    +    with open(src_path, "rb") as f:
             res = requests.put(urlstr, data=f)
         raise_for_status_and_print_error(res)
     
    @@ -82,39 +82,39 @@ 

    Source code for gen3.wss

     
    [docs] class Gen3WsStorage: - """A class for interacting with the Gen3 workspace storage service. + """A class for interacting with the Gen3 workspace storage service. Examples: This generates the Gen3WsStorage class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page. - >>> auth = Gen3Auth(endpoint, refresh_file="credentials.json") + >>> auth = Gen3Auth(endpoint, refresh_file="credentials.json") ... wss = Gen3WsStorage(auth) - """ + """ def __init__(self, auth_provider=None): - """ + """ Initialization for instance of the class to setup basic endpoint info. Args: auth_provider (Gen3Auth, optional): Gen3Auth class to handle passing your token, required for admin endpoints - """ + """ self._auth_provider = auth_provider
    [docs] @backoff.on_exception(backoff.expo, requests.HTTPError, **DEFAULT_BACKOFF_SETTINGS) def upload_url(self, ws, wskey): - """ + """ Get a upload url for the given workspace key Args: ws (string): name of the workspace wskey (string): key of the object in the workspace - """ - wskey = wskey.lstrip("/") - res = self._auth_provider.curl("/ws-storage/upload/{}/{}".format(ws, wskey)) + """ + wskey = wskey.lstrip("/") + res = self._auth_provider.curl("/ws-storage/upload/{}/{}".format(ws, wskey)) raise_for_status_and_print_error(res) return res.json()
    @@ -123,10 +123,10 @@

    Source code for gen3.wss

     [docs]
         @backoff.on_exception(backoff.expo, requests.HTTPError, **DEFAULT_BACKOFF_SETTINGS)
         def upload(self, src_path, dest_ws, dest_wskey):
    -        """
    +        """
             Upload a local file to the specified workspace path
    -        """
    -        url = self.upload_url(dest_ws, dest_wskey)["Data"]
    +        """
    +        url = self.upload_url(dest_ws, dest_wskey)["Data"]
             put_url(url, src_path)
    @@ -134,15 +134,15 @@

    Source code for gen3.wss

     [docs]
         @backoff.on_exception(backoff.expo, requests.HTTPError, **DEFAULT_BACKOFF_SETTINGS)
         def download_url(self, ws, wskey):
    -        """
    +        """
             Get a download url for the given workspace key
     
             Args:
               ws (string): name of the workspace
               wskey (string): key of the object in the workspace
    -        """
    -        wskey = wskey.lstrip("/")
    -        res = self._auth_provider.curl("/ws-storage/download/{}/{}".format(ws, wskey))
    +        """
    +        wskey = wskey.lstrip("/")
    +        res = self._auth_provider.curl("/ws-storage/download/{}/{}".format(ws, wskey))
             raise_for_status_and_print_error(res)
             return res.json()
    @@ -150,51 +150,51 @@

    Source code for gen3.wss

     
    [docs] def download(self, src_ws, src_wskey, dest_path): - """ + """ Download a file from the workspace to local disk Args: src_ws (string): name of the workspace src_wskey (string): key of the object in the workspace dest_path (string): to download the file to - """ - durl = self.download_url(src_ws, src_wskey)["Data"] + """ + durl = self.download_url(src_ws, src_wskey)["Data"] get_url(durl, dest_path)
    [docs] def copy(self, src_urlstr, dest_urlstr): - """ + """ Parse src_urlstr and dest_urlstr, and call upload or download as appropriate - """ - if src_urlstr[0:3] == "ws:": - if dest_urlstr[0:3] == "ws:": + """ + if src_urlstr[0:3] == "ws:": + if dest_urlstr[0:3] == "ws:": raise Exception( - "source and destination may not both reference a workspace" + "source and destination may not both reference a workspace" ) pathparts = wsurl_to_tokens(src_urlstr) return self.download(pathparts[0], pathparts[1], dest_urlstr) - if dest_urlstr[0:3] == "ws:": + if dest_urlstr[0:3] == "ws:": pathparts = wsurl_to_tokens(dest_urlstr) return self.upload(src_urlstr, pathparts[0], pathparts[1]) - raise Exception("source and destination may not both be local")
    + raise Exception("source and destination may not both be local")
    [docs] @backoff.on_exception(backoff.expo, requests.HTTPError, **DEFAULT_BACKOFF_SETTINGS) def ls(self, ws, wskey): - """ + """ List the contents under the given workspace path Args: ws (string): name of the workspace wskey (string): key of the object in the workspace - """ - wskey = wskey.lstrip("/") - res = self._auth_provider.curl("/ws-storage/list/{}/{}".format(ws, wskey)) + """ + wskey = wskey.lstrip("/") + res = self._auth_provider.curl("/ws-storage/list/{}/{}".format(ws, wskey)) raise_for_status_and_print_error(res) return res.json()
    @@ -202,14 +202,14 @@

    Source code for gen3.wss

     
    [docs] def ls_path(self, ws_urlstr): - """ + """ Same as ls - but parses ws_urlstr argument of form: ws:///workspace/key Args: ws (string): name of the workspace wskey (string): key of the object in the workspace - """ + """ pathparts = wsurl_to_tokens(ws_urlstr) return self.ls(pathparts[0], pathparts[1])
    @@ -218,16 +218,16 @@

    Source code for gen3.wss

     [docs]
         @backoff.on_exception(backoff.expo, requests.HTTPError, **DEFAULT_BACKOFF_SETTINGS)
         def rm(self, ws, wskey):
    -        """
    +        """
             Remove the given workspace key
     
             Args:
               ws (string): name of the workspace
               wskey (string): key of the object in the workspace
    -        """
    -        wskey = wskey.lstrip("/")
    +        """
    +        wskey = wskey.lstrip("/")
             res = self._auth_provider.curl(
    -            "/ws-storage/list/{}/{}".format(ws, wskey), request="DELETE"
    +            "/ws-storage/list/{}/{}".format(ws, wskey), request="DELETE"
             )
             raise_for_status_and_print_error(res)
             return res.json()
    @@ -236,9 +236,9 @@

    Source code for gen3.wss

     
    [docs] def rm_path(self, ws_urlstr): - """ + """ Same as rm - but parses the ws_urlstr argument - """ + """ pathparts = wsurl_to_tokens(ws_urlstr) return self.rm(pathparts[0], pathparts[1])
    diff --git a/docs/_build/html/auth.html b/docs/_build/html/auth.html index 74fd6562b..df6616189 100644 --- a/docs/_build/html/auth.html +++ b/docs/_build/html/auth.html @@ -73,11 +73,11 @@

    Gen3 Auth Helper
    >>> auth = Gen3Auth(refresh_file="crdc")
    +
    >>> auth = Gen3Auth(refresh_file="crdc")
     

    or use some arbitrary file:

    -
    diff --git a/docs/_build/html/file.html b/docs/_build/html/file.html index cb970d072..11b4ace13 100644 --- a/docs/_build/html/file.html +++ b/docs/_build/html/file.html @@ -50,7 +50,7 @@

    Gen3 File ClassExamples

    This generates the Gen3File class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page.

    -
    >>> auth = Gen3Auth(refresh_file="credentials.json")
    +
    >>> auth = Gen3Auth(refresh_file="credentials.json")
     ... file = Gen3File(auth)
     
    diff --git a/docs/_build/html/indexing.html b/docs/_build/html/indexing.html index 4448e4609..3b08eb29d 100644 --- a/docs/_build/html/indexing.html +++ b/docs/_build/html/indexing.html @@ -51,7 +51,7 @@

    Gen3 Index ClassExamples

    This generates the Gen3Index class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page.

    -
    >>> auth = Gen3Auth(refresh_file="credentials.json")
    +
    >>> auth = Gen3Auth(refresh_file="credentials.json")
     ... index = Gen3Index(auth)
     
    diff --git a/docs/_build/html/jobs.html b/docs/_build/html/jobs.html index ef4d7609e..2184a792c 100644 --- a/docs/_build/html/jobs.html +++ b/docs/_build/html/jobs.html @@ -43,7 +43,7 @@

    Gen3 Jobs ClassExamples

    This generates the Gen3Jobs class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page.

    -
    >>> auth = Gen3Auth(refresh_file="credentials.json")
    +
    >>> auth = Gen3Auth(refresh_file="credentials.json")
     ... jobs = Gen3Jobs(auth)
     
    diff --git a/docs/_build/html/metadata.html b/docs/_build/html/metadata.html index 182b95cb4..868e4e436 100644 --- a/docs/_build/html/metadata.html +++ b/docs/_build/html/metadata.html @@ -43,7 +43,7 @@

    Gen3 Metadata ClassExamples

    This generates the Gen3Metadata class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page.

    -
    >>> auth = Gen3Auth(refresh_file="credentials.json")
    +
    >>> auth = Gen3Auth(refresh_file="credentials.json")
     ... metadata = Gen3Metadata(auth)
     
    diff --git a/docs/_build/html/object.html b/docs/_build/html/object.html index cc6480453..1bbada433 100644 --- a/docs/_build/html/object.html +++ b/docs/_build/html/object.html @@ -50,7 +50,7 @@

    Gen3 Object ClassExamples

    This generates the Gen3Object class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page.

    -
    >>> auth = Gen3Auth(refresh_file="credentials.json")
    +
    >>> auth = Gen3Auth(refresh_file="credentials.json")
     ... object = Gen3Object(auth)
     
    diff --git a/docs/_build/html/query.html b/docs/_build/html/query.html index 94d535d17..9d60a0b4d 100644 --- a/docs/_build/html/query.html +++ b/docs/_build/html/query.html @@ -48,7 +48,7 @@

    Gen3 Query ClassExamples

    This generates the Gen3Query class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page.

    -
    >>> auth = Gen3Auth(endpoint, refresh_file="credentials.json")
    +
    >>> auth = Gen3Auth(endpoint, refresh_file="credentials.json")
     ... query = Gen3Query(auth)
     
    @@ -71,7 +71,7 @@

    Gen3 Query ClassExamples

    -
    >>> query_string = "{ my_index { my_field } }"
    +
    >>> query_string = "{ my_index { my_field } }"
     ... Gen3Query.graphql_query(query_string)
     
    @@ -103,14 +103,14 @@

    Gen3 Query ClassExamples

    >>> Gen3Query.query(
    -    data_type="subject",
    +    data_type="subject",
         first=50,
         fields=[
    -        "vital_status",
    -        "submitter_id",
    +        "vital_status",
    +        "submitter_id",
         ],
    -    filters={"vital_status": "Alive"},
    -    sort_object={"submitter_id": "asc"},
    +    filters={"vital_status": "Alive"},
    +    sort_object={"submitter_id": "asc"},
     )
     
    @@ -141,15 +141,15 @@

    Gen3 Query ClassExamples

    >>> Gen3Query.raw_data_download(
    -        data_type="subject",
    +        data_type="subject",
             fields=[
    -            "vital_status",
    -            "submitter_id",
    -            "project_id"
    +            "vital_status",
    +            "submitter_id",
    +            "project_id"
             ],
    -        filter_object={"=": {"project_id": "my_program-my_project"}},
    -        sort_fields=[{"submitter_id": "asc"}],
    -        accessibility="accessible"
    +        filter_object={"=": {"project_id": "my_program-my_project"}},
    +        sort_fields=[{"submitter_id": "asc"}],
    +        accessibility="accessible"
         )
     
    diff --git a/docs/_build/html/submission.html b/docs/_build/html/submission.html index b9c4f829e..485ffbcd4 100644 --- a/docs/_build/html/submission.html +++ b/docs/_build/html/submission.html @@ -51,7 +51,7 @@

    Gen3 Submission ClassExamples

    This generates the Gen3Submission class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page.

    -

    @@ -104,7 +104,7 @@

    Gen3 Submission Class

    Examples

    This deletes a node from the CCLE project in the sandbox commons.

    -
    >>> Gen3Submission.delete_node("DCF", "CCLE", "demographic")
    +
    >>> Gen3Submission.delete_node("DCF", "CCLE", "demographic")
     
    @@ -130,7 +130,7 @@

    Gen3 Submission Class

    Examples

    This deletes a list of nodes from the CCLE project in the sandbox commons.

    -
    >>> Gen3Submission.delete_nodes("DCF", "CCLE", ["demographic", "subject", "experiment"])
    +
    >>> Gen3Submission.delete_nodes("DCF", "CCLE", ["demographic", "subject", "experiment"])
     
    @@ -147,7 +147,7 @@

    Gen3 Submission Class

    Examples

    This deletes the “DCF” program.

    -
    >>> Gen3Submission.delete_program("DCF")
    +
    >>> Gen3Submission.delete_program("DCF")
     
    @@ -167,7 +167,7 @@

    Gen3 Submission Class

    Examples

    This deletes the “CCLE” project from the “DCF” program.

    -
    >>> Gen3Submission.delete_project("DCF", "CCLE")
    +
    >>> Gen3Submission.delete_project("DCF", "CCLE")
     
    @@ -187,7 +187,7 @@

    Gen3 Submission Class

    Examples

    This deletes a record from the CCLE project in the sandbox commons.

    -
    >>> Gen3Submission.delete_record("DCF", "CCLE", uuid)
    +
    >>> Gen3Submission.delete_record("DCF", "CCLE", uuid)
     
    @@ -210,7 +210,7 @@

    Gen3 Submission Class

    Examples

    This deletes a list of records from the CCLE project in the sandbox commons.

    -
    >>> Gen3Submission.delete_records("DCF", "CCLE", ["uuid1", "uuid2"])
    +
    >>> Gen3Submission.delete_records("DCF", "CCLE", ["uuid1", "uuid2"])
     
    @@ -232,7 +232,7 @@

    Gen3 Submission Class

    Examples

    This exports all records in the “sample” node from the CCLE project in the sandbox commons.

    -
    >>> Gen3Submission.export_node("DCF", "CCLE", "sample", "tsv", filename="DCF-CCLE_sample_node.tsv")
    +
    >>> Gen3Submission.export_node("DCF", "CCLE", "sample", "tsv", filename="DCF-CCLE_sample_node.tsv")
     
    @@ -254,7 +254,7 @@

    Gen3 Submission Class

    Examples

    This exports a single record from the sandbox commons.

    -
    >>> Gen3Submission.export_record("DCF", "CCLE", "d70b41b9-6f90-4714-8420-e043ab8b77b9", "json", filename="DCF-CCLE_one_record.json")
    +
    >>> Gen3Submission.export_record("DCF", "CCLE", "d70b41b9-6f90-4714-8420-e043ab8b77b9", "json", filename="DCF-CCLE_one_record.json")
     
    @@ -283,7 +283,7 @@

    Gen3 Submission Class

    Examples

    This returns the dictionary schema the “subject” node.

    -
    >>> Gen3Submission.get_dictionary_node("subject")
    +
    >>> Gen3Submission.get_dictionary_node("subject")
     
    @@ -319,7 +319,7 @@

    Gen3 Submission Class

    Example

    -
    >>> Gen3Submission.get_project_dictionary("DCF", "CCLE")
    +
    >>> Gen3Submission.get_project_dictionary("DCF", "CCLE")
     
    @@ -337,7 +337,7 @@

    Gen3 Submission Class

    Example

    -
    >>> Gen3Submission.get_project_manifest("DCF", "CCLE")
    +
    >>> Gen3Submission.get_project_manifest("DCF", "CCLE")
     
    @@ -353,7 +353,7 @@

    Gen3 Submission Class

    Example

    This lists all the projects under the DCF program

    -
    >>> Gen3Submission.get_projects("DCF")
    +
    >>> Gen3Submission.get_projects("DCF")
     
    @@ -371,7 +371,7 @@

    Gen3 Submission Class

    Example

    -
    >>> Gen3Submission.get_project_manifest("DCF", "CCLE")
    +
    >>> Gen3Submission.get_project_manifest("DCF", "CCLE")
     
    @@ -392,7 +392,7 @@

    Gen3 Submission ClassExamples

    This executes a query to get the list of all the project codes for all the projects in the Data Commons.

    -
    >>> query = "{ project(first:0) { code } }"
    +
    >>> query = "{ project(first:0) { code } }"
     ... Gen3Submission.query(query)
     
    @@ -414,7 +414,7 @@

    Gen3 Submission Class

    Examples

    This submits a spreadsheet file containing multiple records in rows to the CCLE project in the sandbox commons.

    -
    >>> Gen3Submission.submit_file("DCF-CCLE","data_spreadsheet.tsv")
    +
    >>> Gen3Submission.submit_file("DCF-CCLE","data_spreadsheet.tsv")
     
    @@ -434,7 +434,7 @@

    Gen3 Submission Class

    Examples

    This submits records to the CCLE project in the sandbox commons.

    -
    >>> Gen3Submission.submit_record("DCF", "CCLE", json)
    +
    >>> Gen3Submission.submit_record("DCF", "CCLE", json)
     
    diff --git a/docs/_build/html/tools/drs_pull.html b/docs/_build/html/tools/drs_pull.html index 84cb57931..1c5e32883 100644 --- a/docs/_build/html/tools/drs_pull.html +++ b/docs/_build/html/tools/drs_pull.html @@ -41,12 +41,12 @@
    Examples:

    This generates the Gen3Jobs class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page.

    -
    >>> datafiles = Manifest.load('sample/manifest_1.json')
    -    downloadManager = DownloadManager("source.my_commons.org",
    -                      Gen3Auth(refresh_file="~.gen3/my_credentials.json"), datafiles)
    +
    >>> datafiles = Manifest.load('sample/manifest_1.json')
    +    downloadManager = DownloadManager("source.my_commons.org",
    +                      Gen3Auth(refresh_file="~.gen3/my_credentials.json"), datafiles)
         for i in datafiles:
             print(i)
    -    downloadManager.download(datafiles, ".")
    +    downloadManager.download(datafiles, ".")
     

    See docs/howto/drsDownloading.md for more details

    diff --git a/docs/_build/html/wss.html b/docs/_build/html/wss.html index d373f5e2d..2e2d17591 100644 --- a/docs/_build/html/wss.html +++ b/docs/_build/html/wss.html @@ -42,7 +42,7 @@

    Gen3 Workspace StorageExamples

    This generates the Gen3WsStorage class pointed at the sandbox commons while using the credentials.json downloaded from the commons profile page.

    -
    >>> auth = Gen3Auth(endpoint, refresh_file="credentials.json")
    +
    >>> auth = Gen3Auth(endpoint, refresh_file="credentials.json")
     ... wss = Gen3WsStorage(auth)
     
    From a3b1c10bd902cccb773bd18b0de37b5e4093f2e1 Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Mon, 14 Sep 2026 10:32:48 -0500 Subject: [PATCH 5/7] Update docs/howto/nextflow.md Co-authored-by: Pauline Ribeyre <4224001+paulineribeyre@users.noreply.github.com> --- docs/howto/nextflow.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/howto/nextflow.md b/docs/howto/nextflow.md index b66641933..06709a91e 100644 --- a/docs/howto/nextflow.md +++ b/docs/howto/nextflow.md @@ -90,9 +90,7 @@ Run that with `gen3 nextflow run main.nf` and the generated override changes fiv `tes.endpoint` and `aws.client.endpoint` become the local proxy, `tes.oauthToken` and `aws.accessKey` become the task token, and `aws.client.s3PathStyleAccess` is forced on. `workDir`, `region`, `timeout`, `maxErrorRetry`, `plugins` and `process` are yours and are -left as they are. `gen3 -vv nextflow run` logs the generated file's contents; `nextflow -c - - config` prints the merged result. +left as they are. `gen3 -vv nextflow run` logs the generated file's contents; `nextflow -c config` prints the merged result. Two details explain why those five: From 0064eba0862a8c7a69fccfce265a5a08df857606 Mon Sep 17 00:00:00 2001 From: avantol Date: Mon, 14 Sep 2026 11:48:57 -0500 Subject: [PATCH 6/7] feat(headers): cleanup header forwarding --- gen3/dpop.py | 25 ++++++++----------------- tests/dpop/test_dpop.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/gen3/dpop.py b/gen3/dpop.py index 75748a47d..ee3ec2463 100644 --- a/gen3/dpop.py +++ b/gen3/dpop.py @@ -52,11 +52,11 @@ # forwarding the client's `transfer-encoding` or `content-length` would describe a # body we are no longer sending (RFC 9112 6.1 and 6.2). # -# `authorization` / `dpop` are dropped because this proxy replaces them with its own -# credentials - except on the S3 route, see below. +# `dpop` is dropped because this proxy mints its own proof. `authorization` is not: +# the S3 route needs the client's SigV4 header to arrive intact, and the TES route +# overwrites it later. _HEADERS_NOT_FORWARDED = frozenset( { - b"authorization", b"connection", b"content-length", b"dpop", @@ -71,13 +71,6 @@ } ) -# The S3 route is the exception. Gen3's S3 endpoint expects a SigV4-signed request -# and reads the task token out of the `Credential=/...` field of the -# client's `Authorization` header; it rejects anything it cannot parse that way, -# including the `DPoP` scheme. So on that route the client's header is forwarded -# untouched and the proof travels in the `DPoP` header alone. -_HEADERS_NOT_FORWARDED_TO_S3 = _HEADERS_NOT_FORWARDED - {b"authorization"} - # The two services the proxy will route to. Broken out here # to allow expansion in the typing easily if we add more services/endpoints. _SERVICE_TES = "tes" @@ -421,18 +414,16 @@ def _build_headers( Returns: dict[str, str]: Headers to send upstream. """ - is_s3 = service == _SERVICE_S3 - not_forwarded = ( - _HEADERS_NOT_FORWARDED_TO_S3 if is_s3 else _HEADERS_NOT_FORWARDED - ) headers = { k.decode("utf-8"): v.decode("utf-8") for k, v in request_headers - if k.lower() not in not_forwarded + if k.lower() not in _HEADERS_NOT_FORWARDED } - if not is_s3: - headers["Authorization"] = f"DPoP {self._config['TASK_TOKEN']}" + if service != _SERVICE_S3: + # Lowercase to overwrite the client's header rather than sit beside it: + # ASGI header names arrive lowercased + headers["authorization"] = f"DPoP {self._config['TASK_TOKEN']}" headers["DPoP"] = self._generate_proof(method, url) # The Authorization value is a live credential and `-vv` output ends up in # bug reports, so log the proof (useful, single-use) but never the token. diff --git a/tests/dpop/test_dpop.py b/tests/dpop/test_dpop.py index d09628903..8328be495 100644 --- a/tests/dpop/test_dpop.py +++ b/tests/dpop/test_dpop.py @@ -594,6 +594,16 @@ def test_tes_credentials_are_replaced(self, proxy): assert headers["authorization"] == f"DPoP {TASK_TOKEN}" assert headers["dpop"] != "client-supplied-proof" + def test_tes_sends_one_authorization_header(self, proxy): + """ + The client's `Authorization` is replaced on the TES route, not duplicated. + This validates there's no unintentional duplication due to header case. + """ + proxy.get(TES_PATH, headers={"Authorization": "Bearer some-other-token"}) + + names = proxy.upstream.last_request["header_names"] + assert names.count("authorization") == 1 + def test_s3_signature_survives_the_hop(self, proxy): """On the S3 route the client's signed `Authorization` is forwarded as-is.""" # Gen3's S3 endpoint reads the token out of the SigV4 `Credential` field, so @@ -1105,6 +1115,8 @@ async def __call__(self, scope, receive, send) -> None: "path": scope["path"], "query": scope["query_string"].decode(), "headers": {k.decode(): v.decode() for k, v in scope["headers"]}, + # Kept alongside the dict, which collapses a header sent twice with different case. + "header_names": [k.decode().lower() for k, _ in scope["headers"]], "body_length": len(body), } ) From 99bbea4006aa3d2db2bded56a179325f9c41fca2 Mon Sep 17 00:00:00 2001 From: avantol Date: Mon, 14 Sep 2026 16:38:25 -0500 Subject: [PATCH 7/7] fix(dpop): make the proxy itself require task token to be present and match the known env vars for tokens so other processes/users cannot just hit it without also sending auth --- docs/howto/nextflow.md | 17 +++++--- gen3/dpop.py | 92 +++++++++++++++++++++++++++++++++++++++++ tests/dpop/test_dpop.py | 59 +++++++++++++++++++++++--- 3 files changed, 157 insertions(+), 11 deletions(-) diff --git a/docs/howto/nextflow.md b/docs/howto/nextflow.md index 06709a91e..897cd9526 100644 --- a/docs/howto/nextflow.md +++ b/docs/howto/nextflow.md @@ -220,15 +220,20 @@ process. when a nonce is stale. - **Task tokens are scoped.** `--task-token-type` (default `WORKFLOW`) asks Gen3 for a token limited to that purpose. +- **The proxy only serves the task token holder.** It listens on `127.0.0.1`, which is + not a boundary on its own: every process on the host can connect to that port, and so + can a page open in your browser. So the proxy requires each request to present the task + token, the way Nextflow already does - as the TES `oauthToken` and as the S3 access key + ID - and refuses anything else with a 401 before it routes the request. The token is + passed to Nextflow in `GEN3_DPOP_BOUND_TASK_TOKEN`, so it is readable by other processes + running as *you*; it is not readable by other users, and a drive-by request from a + browser cannot guess it. +- **Only TES and S3 are proxied.** `/ga4gh/tes` and `/s3` are the only paths forwarded, + and only to the commons that issued your credentials, so the task token is never sent + to another host. Two limits worth knowing about: -- **The proxy is an unauthenticated local endpoint.** It listens only on `127.0.0.1`, so - it is not reachable from other machines, but any process running as any user on the - same host can send requests through it while it is up. Only `/ga4gh/tes` and `/s3` - paths are proxied, so that reaches those two services rather than the whole commons - API — but within them it acts with your task token. Treat it like an SSH agent socket: - fine on your own workstation, think twice on a shared host. - **The task token is not renewed.** It is fetched once at startup. A pipeline that runs longer than the token's lifetime will start seeing 401s; use `--task-token-expiration` to request a lifetime that covers the run. diff --git a/gen3/dpop.py b/gen3/dpop.py index ee3ec2463..d61fe30a3 100644 --- a/gen3/dpop.py +++ b/gen3/dpop.py @@ -5,6 +5,7 @@ import asyncio from contextlib import contextmanager from datetime import datetime, timezone +import hmac import json import socket import tempfile @@ -102,6 +103,11 @@ _MAX_NONCE_RETRIES = 2 +# Authorization schemes that carry the access token directly, lowercased for a +# case-insensitive match. An S3 client sends an AWS signature instead, with the token as the +# access key ID. +_SCHEMES_CARRYING_THE_TOKEN = ("bearer ", "dpop ") + # Chunk size for replaying a buffered request body upstream. _BODY_CHUNK_BYTES = 1024 * 1024 @@ -293,6 +299,15 @@ async def __call__(self, scope: dict[str, Any], receive, send) -> None: await self._send_error(send, 404, "Not Found") return + # Before routing, so an unauthorized caller cannot learn which paths exist either. + if not self._caller_holds_the_task_token(scope.get("headers", [])): + logging.warning( + "Refusing a request that did not present the task token. This proxy signs " + "what it forwards, so only the process it was started for may use it." + ) + await self._send_error(send, 401, "Unauthorized") + return + method = scope["method"] path = scope["path"] query_string = scope.get("query_string", b"").decode("utf-8") @@ -337,6 +352,35 @@ async def aclose(self) -> None: """Close the shared upstream HTTP client.""" await self._client.aclose() + def _caller_holds_the_task_token( + self, request_headers: list[tuple[bytes, bytes]] + ) -> bool: + """ + Check that a request came from the process this proxy was started for. + + The proxy signs whatever it forwards with the DPoP key, so anything that can reach it + can act as the user for the life of the run. + + Holding the task token is what authorizes a caller. Nextflow already sends it on both + routes - as the TES `oauthToken` and as the S3 access key ID - so requiring it costs + the pipeline nothing. + + Args: + request_headers (list[tuple[bytes, bytes]]): Raw headers from the ASGI scope. + + Returns: + bool: True if the caller presented the task token. + """ + presented = _presented_access_token( + _header_value(request_headers, b"authorization") + ) + if not presented: + return False + # Constant time: the comparison is against a live credential. + return hmac.compare_digest( + presented.encode("utf-8"), str(self._config["TASK_TOKEN"]).encode("utf-8") + ) + def _resolve_upstream_url( self, path: str, query_string: str ) -> tuple[str, Service] | None: @@ -669,6 +713,54 @@ def exchange_api_key_for_task_token( return access_token, nonce +def _header_value(request_headers: list[tuple[bytes, bytes]], name: bytes) -> str: + """ + Read one header out of the raw ASGI header list. + + Args: + request_headers (list[tuple[bytes, bytes]]): Raw headers from the ASGI scope. + name (bytes): Lowercased header name to look for. + + Returns: + str: The header value, or "" if it is absent. + """ + for key, value in request_headers: + if key.lower() == name: + return value.decode("utf-8", "replace") + return "" + + +def _presented_access_token(auth_header: str) -> str | None: + """ + Read the access token out of an Authorization header, whichever way it was presented. + + Understands the same shapes Gen3's S3 endpoint does, so that a header this proxy accepts + is one the upstream would read the same token out of. + + Args: + auth_header (str): Value of the incoming Authorization header. + + Returns: + str | None: The token, or None if the header carries none. + """ + if not auth_header: + return None + + stripped = auth_header.strip() + if stripped.lower().startswith(_SCHEMES_CARRYING_THE_TOKEN): + parts = stripped.split(maxsplit=1) + candidate = parts[1].strip() if len(parts) == 2 else "" + elif "Credential=" in stripped: + candidate = stripped.split("Credential=")[1].split("/")[0] + elif "AWS " in stripped: + candidate = stripped.split("AWS ")[1].split(":")[0] + else: + return None + + # A client acting on behalf of a user appends the user ID to its token. + return candidate.split(";userId=")[0] or None + + def is_proxy_running(port: int, host: str = "127.0.0.1") -> bool: """ Check if a proxy server is running on the given port. diff --git a/tests/dpop/test_dpop.py b/tests/dpop/test_dpop.py index 8328be495..49482a8a8 100644 --- a/tests/dpop/test_dpop.py +++ b/tests/dpop/test_dpop.py @@ -580,12 +580,49 @@ def test_paths_that_escape_their_upstream_base_are_refused( class TestProxyHeaders: """What the proxy sends upstream on behalf of the client.""" + def test_only_the_task_token_holder_may_use_the_proxy(self, proxy): + """ + A caller that does not hold the task token is refused. + """ + for header in (None, "Bearer some-other-token", "Bearer", "Basic abc", ""): + headers = {} if header is None else {"Authorization": header} + # An empty dict would let the helper fill in the token, so ask for a header + # that carries no usable one. + response = proxy.get( + TES_PATH, headers=headers or {"Authorization": "Bearer wrong"} + ) + assert response.status_code == 401, f"accepted {header!r}" + assert proxy.upstream.requests == [], "nothing should have been forwarded" + + def test_the_task_token_is_accepted_however_it_is_presented(self, proxy): + """ + Both routes authorize on the same token: TES sends it behind a scheme, and an S3 + client sends it as the access key ID of an AWS signature. + """ + assert proxy.get(TES_PATH).status_code == 200 + assert ( + proxy.get( + S3_PATH, + headers={ + "Authorization": f"AWS4-HMAC-SHA256 Credential={TASK_TOKEN}" + "/20240101/us-east-1/s3/aws4_request" + }, + ).status_code + == 200 + ) + assert ( + proxy.get( + S3_PATH, headers={"Authorization": f"AWS {TASK_TOKEN}:signature"} + ).status_code + == 200 + ) + def test_tes_credentials_are_replaced(self, proxy): """On the TES route the proxy supplies both credentials itself.""" proxy.get( TES_PATH, headers={ - "Authorization": "Bearer some-other-token", + "Authorization": f"Bearer {TASK_TOKEN}", "DPoP": "client-supplied-proof", }, ) @@ -599,7 +636,7 @@ def test_tes_sends_one_authorization_header(self, proxy): The client's `Authorization` is replaced on the TES route, not duplicated. This validates there's no unintentional duplication due to header case. """ - proxy.get(TES_PATH, headers={"Authorization": "Bearer some-other-token"}) + proxy.get(TES_PATH, headers={"Authorization": f"Bearer {TASK_TOKEN}"}) names = proxy.upstream.last_request["header_names"] assert names.count("authorization") == 1 @@ -1219,9 +1256,17 @@ class _Proxy: url: str def request(self, method: str, path: str, **kwargs) -> requests.Response: - """Send a request through the proxy.""" + """ + Send a request through the proxy. + + Presents the task token the way Nextflow does, unless the caller set its own + Authorization header, since the proxy refuses a caller that does not hold it. + """ kwargs.setdefault("timeout", 60) - return requests.request(method, f"{self.url}{path}", **kwargs) + headers = dict(kwargs.pop("headers", None) or {}) + if not any(name.lower() == "authorization" for name in headers): + headers["Authorization"] = f"Bearer {TASK_TOKEN}" + return requests.request(method, f"{self.url}{path}", headers=headers, **kwargs) def get(self, path: str, **kwargs) -> requests.Response: """GET through the proxy.""" @@ -1239,7 +1284,11 @@ def raw_get_status(self, target: str) -> int: has to be written onto the wire directly to reach the proxy intact. """ port = int(self.url.rsplit(":", 1)[1]) - request = f"GET {target} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\n\r\n" + request = ( + f"GET {target} HTTP/1.1\r\n" + f"Host: 127.0.0.1:{port}\r\n" + f"Authorization: Bearer {TASK_TOKEN}\r\n\r\n" + ) with socket.create_connection(("127.0.0.1", port), timeout=60) as client: client.sendall(request.encode()) status_line = client.recv(64).split(b"\r\n")[0]