From 95bf7bfa7836fca91e887b2368bc04aa8b9ed2a4 Mon Sep 17 00:00:00 2001 From: Joris Wouter Jonkers Date: Tue, 20 Oct 2026 21:50:23 +0200 Subject: [PATCH 1/3] feat(identity): the Principal adapter, read from identity's contract --- .gitignore | 4 + .gitleaks.toml | 2 + .python-version | 1 + README.md | 30 + Taskfile.yml | 30 + contract/REF | 1 + contract/mapping.json | 86 ++ contract/principal.schema.json | 91 +++ .../vectors/access-token/bad-signature.json | 8 + contract/vectors/access-token/expired.json | 8 + contract/vectors/access-token/machine.json | 17 + .../vectors/access-token/minor-version.json | 21 + .../access-token/not-an-access-token.json | 8 + contract/vectors/access-token/session.json | 21 + .../vectors/access-token/unknown-key.json | 8 + .../vectors/access-token/unknown-major.json | 8 + .../vectors/access-token/wrong-audience.json | 8 + .../vectors/access-token/wrong-issuer.json | 8 + contract/vectors/access-token/wrong-typ.json | 8 + contract/vectors/introspection/inactive.json | 8 + .../vectors/introspection/personal-token.json | 36 + contract/vectors/introspection/tool.json | 44 + .../vectors/introspection/unknown-major.json | 25 + .../vectors/introspection/wrong-audience.json | 25 + contract/vectors/jwks.json | 12 + .../vectors/signed-header/bad-signature.json | 17 + contract/vectors/signed-header/expired.json | 17 + contract/vectors/signed-header/machine.json | 26 + .../vectors/signed-header/minor-version.json | 30 + contract/vectors/signed-header/missing.json | 16 + .../vectors/signed-header/personal-token.json | 29 + .../signed-header/session-two-factor.json | 31 + contract/vectors/signed-header/session.json | 30 + .../signed-header/spoofed-plain-headers.json | 30 + contract/vectors/signed-header/tool.json | 33 + .../vectors/signed-header/unknown-key.json | 17 + .../vectors/signed-header/unknown-major.json | 17 + .../vectors/signed-header/wrong-audience.json | 17 + .../vectors/signed-header/wrong-issuer.json | 17 + contract/vectors/signed-header/wrong-typ.json | 17 + mise.toml | 2 + pyproject.toml | 50 ++ scripts/generate_principal.py | 94 +++ scripts/pin-contract.sh | 17 + src/jorisjonkers_commons/__init__.py | 1 + src/jorisjonkers_commons/identity/__init__.py | 53 ++ src/jorisjonkers_commons/identity/asgi.py | 146 ++++ .../identity/principal.py | 56 ++ src/jorisjonkers_commons/identity/reader.py | 308 +++++++ src/jorisjonkers_commons/py.typed | 0 tests/contract.py | 55 ++ tests/test_asgi.py | 197 +++++ tests/test_conformance.py | 58 ++ tests/test_fetch.py | 54 ++ tests/test_principal_type.py | 24 + tests/test_reader.py | 310 +++++++ uv.lock | 768 ++++++++++++++++++ 57 files changed, 3055 insertions(+) create mode 100644 .python-version create mode 100644 contract/REF create mode 100644 contract/mapping.json create mode 100644 contract/principal.schema.json create mode 100644 contract/vectors/access-token/bad-signature.json create mode 100644 contract/vectors/access-token/expired.json create mode 100644 contract/vectors/access-token/machine.json create mode 100644 contract/vectors/access-token/minor-version.json create mode 100644 contract/vectors/access-token/not-an-access-token.json create mode 100644 contract/vectors/access-token/session.json create mode 100644 contract/vectors/access-token/unknown-key.json create mode 100644 contract/vectors/access-token/unknown-major.json create mode 100644 contract/vectors/access-token/wrong-audience.json create mode 100644 contract/vectors/access-token/wrong-issuer.json create mode 100644 contract/vectors/access-token/wrong-typ.json create mode 100644 contract/vectors/introspection/inactive.json create mode 100644 contract/vectors/introspection/personal-token.json create mode 100644 contract/vectors/introspection/tool.json create mode 100644 contract/vectors/introspection/unknown-major.json create mode 100644 contract/vectors/introspection/wrong-audience.json create mode 100644 contract/vectors/jwks.json create mode 100644 contract/vectors/signed-header/bad-signature.json create mode 100644 contract/vectors/signed-header/expired.json create mode 100644 contract/vectors/signed-header/machine.json create mode 100644 contract/vectors/signed-header/minor-version.json create mode 100644 contract/vectors/signed-header/missing.json create mode 100644 contract/vectors/signed-header/personal-token.json create mode 100644 contract/vectors/signed-header/session-two-factor.json create mode 100644 contract/vectors/signed-header/session.json create mode 100644 contract/vectors/signed-header/spoofed-plain-headers.json create mode 100644 contract/vectors/signed-header/tool.json create mode 100644 contract/vectors/signed-header/unknown-key.json create mode 100644 contract/vectors/signed-header/unknown-major.json create mode 100644 contract/vectors/signed-header/wrong-audience.json create mode 100644 contract/vectors/signed-header/wrong-issuer.json create mode 100644 contract/vectors/signed-header/wrong-typ.json create mode 100644 pyproject.toml create mode 100644 scripts/generate_principal.py create mode 100755 scripts/pin-contract.sh create mode 100644 src/jorisjonkers_commons/__init__.py create mode 100644 src/jorisjonkers_commons/identity/__init__.py create mode 100644 src/jorisjonkers_commons/identity/asgi.py create mode 100644 src/jorisjonkers_commons/identity/principal.py create mode 100644 src/jorisjonkers_commons/identity/reader.py create mode 100644 src/jorisjonkers_commons/py.typed create mode 100644 tests/contract.py create mode 100644 tests/test_asgi.py create mode 100644 tests/test_conformance.py create mode 100644 tests/test_fetch.py create mode 100644 tests/test_principal_type.py create mode 100644 tests/test_reader.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 841f4d7..050f1b6 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,7 @@ templates/**/.claude/commands/speckit*.md # Python bytecode __pycache__/ *.py[cod] + +# Python tooling +.venv/ +.coverage diff --git a/.gitleaks.toml b/.gitleaks.toml index a6185fd..ab3d0a4 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -9,6 +9,8 @@ useDefault = true description = "Global allowlist" paths = [ '''(.*?)(example|sample|fixture|test)(.*?)\.(env|json|ya?ml)$''', + # identity's conformance vectors, vendored by scripts/pin-contract.sh: tokens signed by a test key. + '''^contract/vectors/''', ] regexes = [ '''EXAMPLE_[A-Z0-9_]+''', diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/README.md b/README.md index 82c679f..f4c778f 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,36 @@ dependencies = [ ] ``` +## identity + +`PrincipalReader.read` is the one entry point: it takes a request's headers by name and returns the +`Principal`, or raises `PrincipalRefused` with the contract's reason. It reads the Gate's signed +`X-Identity-Principal` header and never the plain `X-Auth-Request-*` ones; without it, it verifies a +JWT access token against the Issuer's keys or introspects an opaque token, keeping each answer for +30 seconds. `IdentityUnavailable` means identity could not be asked, which is not a refusal. + +```python +from jorisjonkers_commons.identity import ( + PrincipalReader, principal_middleware, require_recent_two_factor, require_role, +) + +reader = PrincipalReader("https://auth.jorisjonkers.dev", "wiki", client_secret=secret) +app = principal_middleware(app, reader) # 401 when refused, 503 when identity is down +admin = require_role("admin")(admin_app) # 403 without the role +payments = require_recent_two_factor(5)(pay_app) # 303 to the Gate's step-up, 403 for tokens +``` + +The middleware and the helpers are plain ASGI, so they wrap a Starlette or FastAPI app or any one +of its routes; Starlette's `request.state.principal` holds what the middleware read. + +The Principal type is generated from the contract pinned in `contract/`. To move to a new contract +release, pin it and regenerate: + +```bash +scripts/pin-contract.sh contract/principal/v1.1.0 +PRINCIPAL_UPDATE=true task test +``` + ## Working on it ```bash diff --git a/Taskfile.yml b/Taskfile.yml index f785808..d7d7fbf 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -6,15 +6,37 @@ tasks: cmd: task --list silent: true + sync: + desc: Install the locked dependencies + cmd: uv sync --frozen + lint: desc: All linters + deps: [sync] cmds: + - task: lint:python - task: lint:actions + lint:python: + desc: ruff, ruff format and mypy --strict + cmds: + - uv run ruff check . + - uv run ruff format --check . + - uv run mypy + lint:actions: desc: actionlint on every workflow cmd: actionlint + test: + desc: pytest with a branch coverage gate (pyproject's fail_under) + deps: [sync] + cmd: uv run pytest --cov --cov-report=term-missing + + build: + desc: Build the wheel and sdist consumers install + cmd: uv build --quiet + secrets: desc: Scan the working tree for secrets cmd: gitleaks dir --no-banner --redact . @@ -23,4 +45,12 @@ tasks: desc: Everything CI runs on a pull request cmds: - task: lint + - task: test + - task: build - task: secrets + + fix: + desc: Apply formatters and autofixes + cmds: + - uv run ruff format . + - uv run ruff check --fix . diff --git a/contract/REF b/contract/REF new file mode 100644 index 0000000..1a4e71e --- /dev/null +++ b/contract/REF @@ -0,0 +1 @@ +37506c6 diff --git a/contract/mapping.json b/contract/mapping.json new file mode 100644 index 0000000..4eb062a --- /dev/null +++ b/contract/mapping.json @@ -0,0 +1,86 @@ +{ + "version": "1.1", + "signedHeader": { + "header": "X-Identity-Principal", + "typ": "identity-principal+jwt", + "alg": ["RS256"], + "lifetimeSeconds": 60, + "claims": { + "iss": "the Issuer", + "aud": "the Client the request is for", + "iat": "when the Gate signed it", + "exp": "iat + lifetimeSeconds" + } + }, + "fields": { + "subject": { "claim": "sub", "header": "X-Auth-Request-User" }, + "kind": { "claim": "kind", "header": "X-Auth-Request-Kind" }, + "email": { "claim": "email", "header": "X-Auth-Request-Email" }, + "name": { "claim": "name", "header": "X-Auth-Request-Name" }, + "roles": { "claim": "roles", "header": "X-Auth-Request-Roles", "headerFormat": "comma-separated" }, + "via": { "claim": "via", "header": "X-Auth-Request-Via" }, + "actor": { + "claim": "act", + "claimFormat": "RFC 8693 act: {\"sub\": actor.id, \"name\": actor.name}", + "header": "X-Auth-Request-Actor", + "headerFormat": "actor.name" + }, + "twoFactor": { "claim": "two_factor" }, + "twoFactorAt": { "claim": "two_factor_at", "claimFormat": "NumericDate; the Principal states it as RFC 3339 UTC" }, + "sid": { "claim": "sid" }, + "ver": { "claim": "ver" } + }, + "plainHeaders": { + "rule": "The Gate sets every plain header on every allowed answer, empty when the field has no value, so a caller's copy of a header can never stand in for one the Gate left out. A reader of the contract reads only the signed header.", + "headers": [ + "X-Auth-Request-User", + "X-Auth-Request-Email", + "X-Auth-Request-Name", + "X-Auth-Request-Roles", + "X-Auth-Request-Kind", + "X-Auth-Request-Via", + "X-Auth-Request-Actor" + ] + }, + "stepUp": { + "path": "/.auth/step-up", + "query": { + "minutes": "how recent the two-factor proof must be, 1 to 60", + "rd": "the path on the app's own host to come back to" + }, + "rule": "An app that needs a fresher proof than a Principal states redirects the browser to this path on its own host. The person comes back to rd once the session proved two-factor within minutes, and the first Gate check after it states the new twoFactorAt. A Principal that came by personal-token, tool or machine cannot step up: the app answers 403 without a redirect." + }, + "accessToken": { + "typ": "JWT", + "alg": ["RS256"], + "rule": "Since contract 1.1, a JWT access token for a User or a Machine Identity carries the Principal's claims beside its registered ones (iss, sub, aud, exp, client_id). zitadel/oidc signs access tokens and ID tokens alike as typ JWT, so a reader tells an access token by its client_id claim, which an ID token does not carry.", + "refusals": { + "wrong-typ": "The JWT's typ is not JWT, such as the Gate's signed Principal.", + "not-an-access-token": "The JWT names no client_id, as an ID token does not.", + "wrong-issuer": "iss is not the Issuer.", + "wrong-audience": "aud is not exactly the reader's own Client.", + "expired": "The reader's clock is at or past exp.", + "unknown-key": "No key in the Issuer's JWKS has the JWT's kid.", + "bad-signature": "The signature does not verify with the key the kid names.", + "unknown-major": "ver names a major version the reader does not know, or is absent." + } + }, + "introspection": { + "rule": "Since contract 1.1, identity's answer at /oidc/introspect (RFC 7662) for a Personal Token or a Connected App's token carries the Principal's claims. A reader authenticates as its own Client and may keep an answer for at most 30 seconds.", + "refusals": { + "inactive": "The answer is active: false.", + "wrong-audience": "client_id or aud is not the reader's own Client.", + "unknown-major": "ver names a major version the reader does not know, or is absent." + } + }, + "refusals": { + "missing": "No signed header, whatever plain headers the request carries.", + "wrong-typ": "The JWT's typ is not identity-principal+jwt.", + "wrong-issuer": "iss is not the Issuer.", + "wrong-audience": "aud is not the reader's own Client.", + "expired": "The reader's clock is at or past exp.", + "unknown-key": "No key in the Issuer's JWKS has the JWT's kid.", + "bad-signature": "The signature does not verify with the key the kid names.", + "unknown-major": "ver names a major version the reader does not know." + } +} diff --git a/contract/principal.schema.json b/contract/principal.schema.json new file mode 100644 index 0000000..10193d3 --- /dev/null +++ b/contract/principal.schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/JorisJonkers-dev/identity/blob/main/contract/principal/principal.schema.json", + "title": "Principal", + "description": "Who is calling an application, as identity states it on every channel. Contract major version 1; a minor version only adds optional fields, which a reader ignores when it does not know them.", + "type": "object", + "required": ["subject", "kind", "roles", "via", "twoFactor", "ver"], + "properties": { + "subject": { + "description": "The User's or Machine Identity's id.", + "type": "string", + "minLength": 1 + }, + "kind": { + "description": "Whether a person or a workload is calling.", + "enum": ["user", "machine"] + }, + "email": { + "description": "The User's sign-in email.", + "type": "string", + "format": "email" + }, + "name": { + "description": "The User's name.", + "type": "string" + }, + "roles": { + "description": "The App Roles the caller holds on the Client the request is for, and no other Client's.", + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "via": { + "description": "How the caller signed in: a Single sign-on session, a Personal Token, a tool acting for the User, or a Machine Identity's own credential.", + "enum": ["session", "personal-token", "tool", "machine"] + }, + "actor": { + "description": "For via tool, the tool acting for the User: its registration's client id and the name it registered with.", + "type": "object", + "required": ["id", "name"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "name": { "type": "string" } + } + }, + "twoFactor": { + "description": "Whether the session proved two-factor.", + "type": "boolean" + }, + "twoFactorAt": { + "description": "When the session last proved two-factor.", + "type": "string", + "format": "date-time" + }, + "sid": { + "description": "The Single sign-on session, for via session.", + "type": "string", + "minLength": 1 + }, + "ver": { + "description": "The contract version the Principal was stated in, as major.minor.", + "type": "string", + "pattern": "^1\\.(0|[1-9][0-9]*)$" + } + }, + "allOf": [ + { + "if": { "properties": { "kind": { "const": "user" } } }, + "then": { "required": ["email", "name"], "properties": { "via": { "not": { "const": "machine" } } } } + }, + { + "if": { "properties": { "kind": { "const": "machine" } } }, + "then": { "properties": { "via": { "const": "machine" } } } + }, + { + "if": { "properties": { "via": { "const": "tool" } } }, + "then": { "required": ["actor"] }, + "else": { "not": { "required": ["actor"] } } + }, + { + "if": { "properties": { "via": { "const": "session" } } }, + "then": { "required": ["sid"] }, + "else": { "not": { "required": ["sid"] } } + }, + { + "if": { "properties": { "twoFactor": { "const": true } } }, + "then": { "required": ["twoFactorAt"] }, + "else": { "not": { "required": ["twoFactorAt"] } } + } + ] +} diff --git a/contract/vectors/access-token/bad-signature.json b/contract/vectors/access-token/bad-signature.json new file mode 100644 index 0000000..380b8ad --- /dev/null +++ b/contract/vectors/access-token/bad-signature.json @@ -0,0 +1,8 @@ +{ + "description": "A token whose claims were changed after signing is refused.", + "issuer": "https://auth.example.test", + "client": "web", + "now": "2026-10-20T12:01:00Z", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsid2ViIl0sImNsaWVudF9pZCI6IndlYiIsImVtYWlsIjoiYWRhQGV4YW1wbGUub3JnIiwiZXhwIjoxNzkyNDk4NTAwLCJpYXQiOjE3OTI0OTc2MDAsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLnRlc3QiLCJqdGkiOiJhdC0xIiwia2luZCI6InVzZXIiLCJuYW1lIjoiQWRhIiwibmJmIjoxNzkyNDk3NjAwLCJyb2xlcyI6WyJhZG1pbiJdLCJzaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYzEiLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYTEiLCJ0d29fZmFjdG9yIjp0cnVlLCJ0d29fZmFjdG9yX2F0IjoxNzkyNDk2NzAwLCJ2ZXIiOiIxLjEiLCJ2aWEiOiJzZXNzaW9uIn0.xEqJRTB3ynRjYFaabbr0aZNLKQdRVX6cJ5j3Onhbm4PnvQvwFtoi2YPNWK482B45vqgEcYtGBmhmU2Ve2qu28zHEWAq-94wVDvtnP29-ietc439sTaUv0-SXzsXKYryoZyW32-JtoIP1bLpi5iRQJbm4PuG0QhaTOE7YN4AL-Fd-SKbia7UChRHMMxR7aQ1QKH6zeQDKzJSDYgObob0ah8Dn7FftUBLEDK5xO-aUwDIqyO0SWCm2GaTM9ceZlLlRD_i8GT6OHt63J-0K-4Cu6xWRf9wYHPRzt8Tt6npchsntfx---rsUf_IhULtxO-ibvNfW-4yc1ULVcuWtOPZjqg", + "refusal": "bad-signature" +} diff --git a/contract/vectors/access-token/expired.json b/contract/vectors/access-token/expired.json new file mode 100644 index 0000000..ee28489 --- /dev/null +++ b/contract/vectors/access-token/expired.json @@ -0,0 +1,8 @@ +{ + "description": "A token read at the moment it expires is refused.", + "issuer": "https://auth.example.test", + "client": "web", + "now": "2026-10-20T12:15:00Z", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsid2ViIl0sImNsaWVudF9pZCI6IndlYiIsImVtYWlsIjoiYWRhQGV4YW1wbGUub3JnIiwiZXhwIjoxNzkyNDk4NTAwLCJpYXQiOjE3OTI0OTc2MDAsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLnRlc3QiLCJqdGkiOiJhdC0xIiwia2luZCI6InVzZXIiLCJuYW1lIjoiQWRhIiwibmJmIjoxNzkyNDk3NjAwLCJyb2xlcyI6WyJ1c2VyIl0sInNpZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBjMSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBhMSIsInR3b19mYWN0b3IiOnRydWUsInR3b19mYWN0b3JfYXQiOjE3OTI0OTY3MDAsInZlciI6IjEuMSIsInZpYSI6InNlc3Npb24ifQ.xEqJRTB3ynRjYFaabbr0aZNLKQdRVX6cJ5j3Onhbm4PnvQvwFtoi2YPNWK482B45vqgEcYtGBmhmU2Ve2qu28zHEWAq-94wVDvtnP29-ietc439sTaUv0-SXzsXKYryoZyW32-JtoIP1bLpi5iRQJbm4PuG0QhaTOE7YN4AL-Fd-SKbia7UChRHMMxR7aQ1QKH6zeQDKzJSDYgObob0ah8Dn7FftUBLEDK5xO-aUwDIqyO0SWCm2GaTM9ceZlLlRD_i8GT6OHt63J-0K-4Cu6xWRf9wYHPRzt8Tt6npchsntfx---rsUf_IhULtxO-ibvNfW-4yc1ULVcuWtOPZjqg", + "refusal": "expired" +} diff --git a/contract/vectors/access-token/machine.json b/contract/vectors/access-token/machine.json new file mode 100644 index 0000000..db28203 --- /dev/null +++ b/contract/vectors/access-token/machine.json @@ -0,0 +1,17 @@ +{ + "description": "A Machine Identity's access token: kind machine, and no email or name.", + "issuer": "https://auth.example.test", + "client": "web", + "now": "2026-10-20T12:01:00Z", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsid2ViIl0sImNsaWVudF9pZCI6IndlYiIsImV4cCI6MTc5MjQ5ODUwMCwiaWF0IjoxNzkyNDk3NjAwLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS50ZXN0IiwianRpIjoiYXQtMSIsImtpbmQiOiJtYWNoaW5lIiwibmJmIjoxNzkyNDk3NjAwLCJyb2xlcyI6WyJiYWNrdXAiXSwic3ViIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGI3IiwidHdvX2ZhY3RvciI6ZmFsc2UsInZlciI6IjEuMSIsInZpYSI6Im1hY2hpbmUifQ.PBxoWeNVSQ-Qn9haqOQPuSzDxF3iGBmEStlkhsYbu3Ej2IH5SEQqfmn0IJMDdEE75HmYjHdLQDmjO-UUGMagLzNzoGxSAWK-eM4UKehr99EwjAylIeo4sdzejTtK1PQh7HraPtc9auE55_N8ordk4uX_V4AUxTKU2DQNfUPq1X6JUjhX_iTC_FGkaTrfQ63Khg3pMpVK3elFy7pDDLBKopH6Crb6wb21CFLdyyuaMFCm3on7EnF-MhtV5fgYDyOZYOcmoNC_ilsVSeid3TTm83uTlOHCyogVsvjSh1gDga5oKg5KlH_z--mXEfh1qWribS8AhwaP3iqjK8mvv0tH3w", + "principal": { + "subject": "00000000-0000-4000-8000-0000000000b7", + "kind": "machine", + "roles": [ + "backup" + ], + "via": "machine", + "twoFactor": false, + "ver": "1.1" + } +} diff --git a/contract/vectors/access-token/minor-version.json b/contract/vectors/access-token/minor-version.json new file mode 100644 index 0000000..85bdf9e --- /dev/null +++ b/contract/vectors/access-token/minor-version.json @@ -0,0 +1,21 @@ +{ + "description": "A later minor version's token is read, and the claim the reader does not know is ignored.", + "issuer": "https://auth.example.test", + "client": "web", + "now": "2026-10-20T12:01:00Z", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsid2ViIl0sImNsaWVudF9pZCI6IndlYiIsImVtYWlsIjoiYWRhQGV4YW1wbGUub3JnIiwiZXhwIjoxNzkyNDk4NTAwLCJmdXR1cmUiOiJhIGxhdGVyIGNsYWltIiwiaWF0IjoxNzkyNDk3NjAwLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS50ZXN0IiwianRpIjoiYXQtMSIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSIsIm5iZiI6MTc5MjQ5NzYwMCwicm9sZXMiOlsidXNlciJdLCJzaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYzEiLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYTEiLCJ0d29fZmFjdG9yIjp0cnVlLCJ0d29fZmFjdG9yX2F0IjoxNzkyNDk2NzAwLCJ2ZXIiOiIxLjIiLCJ2aWEiOiJzZXNzaW9uIn0.Y2F3xek1fslaMYVvujZD3rtyXSWaVApP3GXjCJytk9HuMRU49Ij8DdULqFUdAsjiqJL7dUkmSkqx7S6vqC9-ku4gR4sN-GQEanFL2fe_0RV5BZ9dkvm5vEPaELEmFGk8WvjXQGEtVfs8X6_YIRU4vEpKvoFx_WZX6thYF1fT0NUiShZ_fpdOUk8xywL9gut-REV-0etM16xHnpNDyOE8RJRNXc88OLN9pdmwS8egYN4Jn1FDvUYJa2VMVXHycUEPvXKZewq-MB5CLdUvMKGAP7ZieYUbw5wl3vwpQH69g3tlAiaE28IZO9rC3exkxkdIQ9nxjCcmwxwFhIZ2uBAzjw", + "principal": { + "subject": "00000000-0000-4000-8000-0000000000a1", + "kind": "user", + "email": "ada@example.org", + "name": "Ada", + "roles": [ + "user" + ], + "via": "session", + "twoFactor": true, + "twoFactorAt": "2026-10-20T11:45:00Z", + "sid": "00000000-0000-4000-8000-0000000000c1", + "ver": "1.2" + } +} diff --git a/contract/vectors/access-token/not-an-access-token.json b/contract/vectors/access-token/not-an-access-token.json new file mode 100644 index 0000000..926f4b1 --- /dev/null +++ b/contract/vectors/access-token/not-an-access-token.json @@ -0,0 +1,8 @@ +{ + "description": "An ID token, which names no client_id, is no access token.", + "issuer": "https://auth.example.test", + "client": "web", + "now": "2026-10-20T12:01:00Z", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsid2ViIl0sImVtYWlsIjoiYWRhQGV4YW1wbGUub3JnIiwiZXhwIjoxNzkyNDk4NTAwLCJpYXQiOjE3OTI0OTc2MDAsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLnRlc3QiLCJqdGkiOiJhdC0xIiwia2luZCI6InVzZXIiLCJuYW1lIjoiQWRhIiwibmJmIjoxNzkyNDk3NjAwLCJyb2xlcyI6WyJ1c2VyIl0sInNpZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBjMSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBhMSIsInR3b19mYWN0b3IiOnRydWUsInR3b19mYWN0b3JfYXQiOjE3OTI0OTY3MDAsInZlciI6IjEuMSIsInZpYSI6InNlc3Npb24ifQ.Mf2JTjmg707BUvK4T17GKAUs-SeUpszVlBj0Y-zvXz2RG-6d9dWBBurnaQzs6f79mQjZFhCjhKFLd4AQX23xxZWlDiyZcKqNCPs67xRqXZ0njUuDVcM-thnBIdkN4BUk3bEHkHWn49nUqhm2bPnkN-dZx6tbfo9XKlNsH6yGKReLa3qgWZoOGgK_OX1hKcpzg8e67bccGNqwTP76jFIJ9B48SeuGZX8gH_Na0EFad3LJcHKJH1hTshC54kP47eiXjLdOUbwsLZu0bWLZ6RM4SIs8ZnYgMiXBLVKFgB7Y8XOIbjhcuLDzefHGbiPtWkFsG6KfoqiLosSqvrLwIClP4Q", + "refusal": "not-an-access-token" +} diff --git a/contract/vectors/access-token/session.json b/contract/vectors/access-token/session.json new file mode 100644 index 0000000..49db66d --- /dev/null +++ b/contract/vectors/access-token/session.json @@ -0,0 +1,21 @@ +{ + "description": "A User's access token from a sign-in whose session proved two-factor fifteen minutes before.", + "issuer": "https://auth.example.test", + "client": "web", + "now": "2026-10-20T12:01:00Z", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsid2ViIl0sImNsaWVudF9pZCI6IndlYiIsImVtYWlsIjoiYWRhQGV4YW1wbGUub3JnIiwiZXhwIjoxNzkyNDk4NTAwLCJpYXQiOjE3OTI0OTc2MDAsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLnRlc3QiLCJqdGkiOiJhdC0xIiwia2luZCI6InVzZXIiLCJuYW1lIjoiQWRhIiwibmJmIjoxNzkyNDk3NjAwLCJyb2xlcyI6WyJ1c2VyIl0sInNpZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBjMSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBhMSIsInR3b19mYWN0b3IiOnRydWUsInR3b19mYWN0b3JfYXQiOjE3OTI0OTY3MDAsInZlciI6IjEuMSIsInZpYSI6InNlc3Npb24ifQ.xEqJRTB3ynRjYFaabbr0aZNLKQdRVX6cJ5j3Onhbm4PnvQvwFtoi2YPNWK482B45vqgEcYtGBmhmU2Ve2qu28zHEWAq-94wVDvtnP29-ietc439sTaUv0-SXzsXKYryoZyW32-JtoIP1bLpi5iRQJbm4PuG0QhaTOE7YN4AL-Fd-SKbia7UChRHMMxR7aQ1QKH6zeQDKzJSDYgObob0ah8Dn7FftUBLEDK5xO-aUwDIqyO0SWCm2GaTM9ceZlLlRD_i8GT6OHt63J-0K-4Cu6xWRf9wYHPRzt8Tt6npchsntfx---rsUf_IhULtxO-ibvNfW-4yc1ULVcuWtOPZjqg", + "principal": { + "subject": "00000000-0000-4000-8000-0000000000a1", + "kind": "user", + "email": "ada@example.org", + "name": "Ada", + "roles": [ + "user" + ], + "via": "session", + "twoFactor": true, + "twoFactorAt": "2026-10-20T11:45:00Z", + "sid": "00000000-0000-4000-8000-0000000000c1", + "ver": "1.1" + } +} diff --git a/contract/vectors/access-token/unknown-key.json b/contract/vectors/access-token/unknown-key.json new file mode 100644 index 0000000..5e0459f --- /dev/null +++ b/contract/vectors/access-token/unknown-key.json @@ -0,0 +1,8 @@ +{ + "description": "A token signed by a key the Issuer's JWKS does not publish is refused.", + "issuer": "https://auth.example.test", + "client": "web", + "now": "2026-10-20T12:01:00Z", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1zdHJhbmdlciIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsid2ViIl0sImNsaWVudF9pZCI6IndlYiIsImVtYWlsIjoiYWRhQGV4YW1wbGUub3JnIiwiZXhwIjoxNzkyNDk4NTAwLCJpYXQiOjE3OTI0OTc2MDAsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLnRlc3QiLCJqdGkiOiJhdC0xIiwia2luZCI6InVzZXIiLCJuYW1lIjoiQWRhIiwibmJmIjoxNzkyNDk3NjAwLCJyb2xlcyI6WyJ1c2VyIl0sInNpZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBjMSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBhMSIsInR3b19mYWN0b3IiOnRydWUsInR3b19mYWN0b3JfYXQiOjE3OTI0OTY3MDAsInZlciI6IjEuMSIsInZpYSI6InNlc3Npb24ifQ.Tc49Nle3ReDQbx3fVtnqOYCs85ZZKIRyWvIDoVXvCIPDd0TKi12uknBSn9Ebo9gviKMnZV3LF8ujVWksx3XoHfyE77lfepGTgu532W6-lC21U43HBY30kDlLpsa9hT1_p-4B6Ivxaip2UxZzN1DGifbRxIPBH3Ed6ZWbziM4MnHk2iIBpkoj2boFTcfLcpqqm4tC0_omqX72P9Hgzianc7F8KtuWN-h1UJ9LcxYiQPJOl35CUx-AxqXaBKd5jDn_n4c0o5eUpanTsoeFUCN78o41eL-ToZWfhuK5Zwk2RT91IyMe7Fn3Pe4oHoxKUvZNOgr7fccT0G2ZyBc3WdnvvQ", + "refusal": "unknown-key" +} diff --git a/contract/vectors/access-token/unknown-major.json b/contract/vectors/access-token/unknown-major.json new file mode 100644 index 0000000..a3a0f69 --- /dev/null +++ b/contract/vectors/access-token/unknown-major.json @@ -0,0 +1,8 @@ +{ + "description": "A token stated in a major version the reader does not know is refused.", + "issuer": "https://auth.example.test", + "client": "web", + "now": "2026-10-20T12:01:00Z", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsid2ViIl0sImNsaWVudF9pZCI6IndlYiIsImVtYWlsIjoiYWRhQGV4YW1wbGUub3JnIiwiZXhwIjoxNzkyNDk4NTAwLCJpYXQiOjE3OTI0OTc2MDAsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLnRlc3QiLCJqdGkiOiJhdC0xIiwia2luZCI6InVzZXIiLCJuYW1lIjoiQWRhIiwibmJmIjoxNzkyNDk3NjAwLCJyb2xlcyI6WyJ1c2VyIl0sInNpZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBjMSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBhMSIsInR3b19mYWN0b3IiOnRydWUsInR3b19mYWN0b3JfYXQiOjE3OTI0OTY3MDAsInZlciI6IjIuMCIsInZpYSI6InNlc3Npb24ifQ.2K1SA_JfQVHCLdU790PTeqeBs7rEcfAgv6RPJiNVFlLqen_Yoo1t6YBCsshNooogxaLSVFUAOreliIZR3doj22YiWPTMFUApcyPEsqJfaCRl2EmpLRw_X20ggQmy54hN3ravBWE8ytQaSkE6vVIDLVa0ky3MAAJKe89WNci_KninMqPlWSPvTPoHll0Cu4D2JWFVWsyGB0JA6oMyulbQ4fEFDqgiEiAYLWL8y_AZadWqvDKUUn5KQO-ZkFYGCdiDGmZEO9WuyHJ-BO48KwVQObVBvl7dM19fUZN3hYHhw_aQLZSbl1Xu_rp2q-X3ij8WmiqGzqR4B4qtob4JUl8EEQ", + "refusal": "unknown-major" +} diff --git a/contract/vectors/access-token/wrong-audience.json b/contract/vectors/access-token/wrong-audience.json new file mode 100644 index 0000000..8c03bcb --- /dev/null +++ b/contract/vectors/access-token/wrong-audience.json @@ -0,0 +1,8 @@ +{ + "description": "A token for another Client is refused.", + "issuer": "https://auth.example.test", + "client": "web", + "now": "2026-10-20T12:01:00Z", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiZ3JhZmFuYSJdLCJjbGllbnRfaWQiOiJ3ZWIiLCJlbWFpbCI6ImFkYUBleGFtcGxlLm9yZyIsImV4cCI6MTc5MjQ5ODUwMCwiaWF0IjoxNzkyNDk3NjAwLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS50ZXN0IiwianRpIjoiYXQtMSIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSIsIm5iZiI6MTc5MjQ5NzYwMCwicm9sZXMiOlsidXNlciJdLCJzaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYzEiLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYTEiLCJ0d29fZmFjdG9yIjp0cnVlLCJ0d29fZmFjdG9yX2F0IjoxNzkyNDk2NzAwLCJ2ZXIiOiIxLjEiLCJ2aWEiOiJzZXNzaW9uIn0.CzOwTmQFtgN6601UclDmgikOuUX8BWMTM1U1VJAHE69KEjKeEgZOwZ3B6iGwT6046_WHhaXYUh4pDJYWqhw5taSJ0v5_GJJTCxTLW0-YTcNGNQY2XCki0RGq3SG1IBA8lqrmQBVmUcdDuu1ebH-VxSB9KsBN9MO6fEj2UwVfolqkdDVCGD1laEwRv2eqUezOuxHi_ofzKaAeIkg20cUD7aQDo3aYTr_7qk_-H25XPrTM_Le1jYM2A4FVdk3EJGZV3KnRazPL6xNDGOlJajQS-T9-deOpQucz_ZewsN3-YefEFk7aRLTud2fFPT7yaZAw9wIWjdc86BMtbNasNR6I5w", + "refusal": "wrong-audience" +} diff --git a/contract/vectors/access-token/wrong-issuer.json b/contract/vectors/access-token/wrong-issuer.json new file mode 100644 index 0000000..f743910 --- /dev/null +++ b/contract/vectors/access-token/wrong-issuer.json @@ -0,0 +1,8 @@ +{ + "description": "A token another issuer signed is refused.", + "issuer": "https://auth.example.test", + "client": "web", + "now": "2026-10-20T12:01:00Z", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsid2ViIl0sImNsaWVudF9pZCI6IndlYiIsImVtYWlsIjoiYWRhQGV4YW1wbGUub3JnIiwiZXhwIjoxNzkyNDk4NTAwLCJpYXQiOjE3OTI0OTc2MDAsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLm9yZyIsImp0aSI6ImF0LTEiLCJraW5kIjoidXNlciIsIm5hbWUiOiJBZGEiLCJuYmYiOjE3OTI0OTc2MDAsInJvbGVzIjpbInVzZXIiXSwic2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGMxIiwic3ViIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGExIiwidHdvX2ZhY3RvciI6dHJ1ZSwidHdvX2ZhY3Rvcl9hdCI6MTc5MjQ5NjcwMCwidmVyIjoiMS4xIiwidmlhIjoic2Vzc2lvbiJ9.hjNvq1BVxOgPKC9yGfHZUuui637w42EJOWTQoQdT5wEkGTQRJ2blUcazu6E97GLLS7KOjMR-4kJ0p_YA_pF_J1WwfbsjuVif1w6YYgEKRv9c8pUoY3yL4TcvIbOMOsgtopXINB3zXFqIkQWAxnZZrmGDnXDfzGV8nx85Vq3PokL-jZY1z_NPnfgxEGIJM8a8bUW6Zlyue87dINp8W3_MBycNOfgj1xtyVgLbrSLN6VWeHs6tG5I6X5OXpi-aTvu21bRofKGRJPOiYPvzCGy8MCQ-QCTYHqG-cGZi-XkQIHddsdGWSl2zdeozTbm16Rb2x_JXpQdc_muPIayu9YyKIQ", + "refusal": "wrong-issuer" +} diff --git a/contract/vectors/access-token/wrong-typ.json b/contract/vectors/access-token/wrong-typ.json new file mode 100644 index 0000000..5fba107 --- /dev/null +++ b/contract/vectors/access-token/wrong-typ.json @@ -0,0 +1,8 @@ +{ + "description": "The Gate's signed Principal, or any JWT not typed JWT, is no access token.", + "issuer": "https://auth.example.test", + "client": "web", + "now": "2026-10-20T12:01:00Z", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOlsid2ViIl0sImNsaWVudF9pZCI6IndlYiIsImVtYWlsIjoiYWRhQGV4YW1wbGUub3JnIiwiZXhwIjoxNzkyNDk4NTAwLCJpYXQiOjE3OTI0OTc2MDAsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLnRlc3QiLCJqdGkiOiJhdC0xIiwia2luZCI6InVzZXIiLCJuYW1lIjoiQWRhIiwibmJmIjoxNzkyNDk3NjAwLCJyb2xlcyI6WyJ1c2VyIl0sInNpZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBjMSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBhMSIsInR3b19mYWN0b3IiOnRydWUsInR3b19mYWN0b3JfYXQiOjE3OTI0OTY3MDAsInZlciI6IjEuMSIsInZpYSI6InNlc3Npb24ifQ.gGiY7c8PXRG75alXcsGdhJiVPC0g7Aqr98IesVDyvtOGmYjMPq7jrUHWvMOOGn38tNrUBNK6Oyk-qaw9bCPjlmgX_35sa_zFDnK7l1G-hS-DWMyUUzKhvuctA56N6b37xAPO6tKnN61AK7lDMAxMI7UCT3gII3gVNeX7PT0x9f-WG-Al21fJfXoXIz4KG2mLHMkfhhgP-FSfWTo2bZ0nPgd9hQV9SY5XksDnMbXVkz_bDvunRvl83ZQkBUSrw1LgF7f-fp45oacSbeZgtphM1aPoesraxlfgGxI2FBcnwcEBXN_-pgqnmOUznxDkSs7QaFmczL41plDGVkKCGatWjQ", + "refusal": "wrong-typ" +} diff --git a/contract/vectors/introspection/inactive.json b/contract/vectors/introspection/inactive.json new file mode 100644 index 0000000..e59fe4d --- /dev/null +++ b/contract/vectors/introspection/inactive.json @@ -0,0 +1,8 @@ +{ + "description": "A token identity answers inactive carries no Principal.", + "client": "web", + "answer": { + "active": false + }, + "refusal": "inactive" +} diff --git a/contract/vectors/introspection/personal-token.json b/contract/vectors/introspection/personal-token.json new file mode 100644 index 0000000..4a1af37 --- /dev/null +++ b/contract/vectors/introspection/personal-token.json @@ -0,0 +1,36 @@ +{ + "description": "A Personal Token: no session, and no two-factor.", + "client": "web", + "answer": { + "active": true, + "aud": [ + "web" + ], + "client_id": "web", + "email": "ada@example.org", + "email_verified": true, + "exp": 1795089600, + "kind": "user", + "name": "Ada", + "roles": [ + "user" + ], + "sub": "00000000-0000-4000-8000-0000000000a1", + "token_type": "Bearer", + "two_factor": false, + "ver": "1.1", + "via": "personal-token" + }, + "principal": { + "subject": "00000000-0000-4000-8000-0000000000a1", + "kind": "user", + "email": "ada@example.org", + "name": "Ada", + "roles": [ + "user" + ], + "via": "personal-token", + "twoFactor": false, + "ver": "1.1" + } +} diff --git a/contract/vectors/introspection/tool.json b/contract/vectors/introspection/tool.json new file mode 100644 index 0000000..695ad53 --- /dev/null +++ b/contract/vectors/introspection/tool.json @@ -0,0 +1,44 @@ +{ + "description": "A Connected App's token, naming the tool acting for the User as act.", + "client": "web", + "answer": { + "act": { + "name": "Claude Code", + "sub": "mcp_6f1d0c2e" + }, + "active": true, + "aud": [ + "web" + ], + "client_id": "web", + "email": "ada@example.org", + "email_verified": true, + "exp": 1795089600, + "kind": "user", + "name": "Ada", + "roles": [ + "user" + ], + "sub": "00000000-0000-4000-8000-0000000000a1", + "token_type": "Bearer", + "two_factor": false, + "ver": "1.1", + "via": "tool" + }, + "principal": { + "subject": "00000000-0000-4000-8000-0000000000a1", + "kind": "user", + "email": "ada@example.org", + "name": "Ada", + "roles": [ + "user" + ], + "via": "tool", + "actor": { + "id": "mcp_6f1d0c2e", + "name": "Claude Code" + }, + "twoFactor": false, + "ver": "1.1" + } +} diff --git a/contract/vectors/introspection/unknown-major.json b/contract/vectors/introspection/unknown-major.json new file mode 100644 index 0000000..598a0ea --- /dev/null +++ b/contract/vectors/introspection/unknown-major.json @@ -0,0 +1,25 @@ +{ + "description": "An answer stated in a major version the reader does not know is refused.", + "client": "web", + "answer": { + "active": true, + "aud": [ + "web" + ], + "client_id": "web", + "email": "ada@example.org", + "email_verified": true, + "exp": 1795089600, + "kind": "user", + "name": "Ada", + "roles": [ + "user" + ], + "sub": "00000000-0000-4000-8000-0000000000a1", + "token_type": "Bearer", + "two_factor": false, + "ver": "2.0", + "via": "personal-token" + }, + "refusal": "unknown-major" +} diff --git a/contract/vectors/introspection/wrong-audience.json b/contract/vectors/introspection/wrong-audience.json new file mode 100644 index 0000000..7e4bd6e --- /dev/null +++ b/contract/vectors/introspection/wrong-audience.json @@ -0,0 +1,25 @@ +{ + "description": "An answer for another Client is refused.", + "client": "web", + "answer": { + "active": true, + "aud": [ + "grafana" + ], + "client_id": "grafana", + "email": "ada@example.org", + "email_verified": true, + "exp": 1795089600, + "kind": "user", + "name": "Ada", + "roles": [ + "user" + ], + "sub": "00000000-0000-4000-8000-0000000000a1", + "token_type": "Bearer", + "two_factor": false, + "ver": "1.1", + "via": "personal-token" + }, + "refusal": "wrong-audience" +} diff --git a/contract/vectors/jwks.json b/contract/vectors/jwks.json new file mode 100644 index 0000000..850f464 --- /dev/null +++ b/contract/vectors/jwks.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "use": "sig", + "kty": "RSA", + "kid": "vector-issuer", + "alg": "RS256", + "n": "3QUB-_bidklXF7tJtPTLuaHeOoJdIkZr3nsEjFwgei6KXZEQBsgtFCsdxhj_7-hwyM15lvsDGtpnfD9inpR1Nghn2ssHkcDVG8AJy1GY_cGaO17wnZpr296aESTEm65VzN7W1rzEKLOGYFdz1Tked4yaQSTQiKT1J82A5Ft89grOAR3yvXdvjnAFq5aHj7PGY6pKf84CJa57JTqGi8yLJX5F0qn8D2fSTcDHZoN2Dof0qsswQdR1LFB5s_teeL44RQF5pZ8PHs2aPM0pgCsXfz6Im5AlrCeQJOtDndLfb2yfiZmsJb_G8JX89wTsB8h3dA0W0HUECbg_7pYUjOV04w", + "e": "AQAB" + } + ] +} diff --git a/contract/vectors/signed-header/bad-signature.json b/contract/vectors/signed-header/bad-signature.json new file mode 100644 index 0000000..a1b93b5 --- /dev/null +++ b/contract/vectors/signed-header/bad-signature.json @@ -0,0 +1,17 @@ +{ + "description": "A Principal whose claims were changed after signing is refused.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "session", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOiJ3aWtpIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImlhdCI6MTc5MjQ5NzYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUudGVzdCIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGVzIjpbImFkbWluIl0sInNpZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBjMSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBhMSIsInR3b19mYWN0b3IiOmZhbHNlLCJ2ZXIiOiIxLjEiLCJ2aWEiOiJzZXNzaW9uIn0.rEbl5FwAq4eMnl41n0tMcZybbUmFBrjjeGYqRRVyqf68kAhpk7cKK6W_Krag1JiDICMWXZxXCPuaHL-28yrSBggg2V9m_g-qh3VEAjVxzTh-_jbkErtyqW9Wl6UTbqPNhrFEU4-jffWqOtl-NPJGh_XmcvtCbM3RUYAC9Hkm-oX8ZFMPqPpoPn4l7kvlFUfq5solAN3VLYw2Z8Qv-pgPt7O5K_dn_xh9f1rWGYqx-VmNUoitvdD7GSXXV1zRMsxGr5ekrNP6ERUmCD6_mCk7KyvlhM6GW3f93lfvlL4gvUDxaOcE-Wa9WAr8zogoX47HeWRgU1NpJYm_eFWtcnp4Nw" + }, + "refusal": "bad-signature" +} diff --git a/contract/vectors/signed-header/expired.json b/contract/vectors/signed-header/expired.json new file mode 100644 index 0000000..11f1485 --- /dev/null +++ b/contract/vectors/signed-header/expired.json @@ -0,0 +1,17 @@ +{ + "description": "A Principal read at the moment it expires is refused.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:01:00Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "session", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOiJ3aWtpIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImlhdCI6MTc5MjQ5NzYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUudGVzdCIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGVzIjpbInVzZXIiLCJlZGl0b3IiXSwic2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGMxIiwic3ViIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGExIiwidHdvX2ZhY3RvciI6ZmFsc2UsInZlciI6IjEuMSIsInZpYSI6InNlc3Npb24ifQ.rEbl5FwAq4eMnl41n0tMcZybbUmFBrjjeGYqRRVyqf68kAhpk7cKK6W_Krag1JiDICMWXZxXCPuaHL-28yrSBggg2V9m_g-qh3VEAjVxzTh-_jbkErtyqW9Wl6UTbqPNhrFEU4-jffWqOtl-NPJGh_XmcvtCbM3RUYAC9Hkm-oX8ZFMPqPpoPn4l7kvlFUfq5solAN3VLYw2Z8Qv-pgPt7O5K_dn_xh9f1rWGYqx-VmNUoitvdD7GSXXV1zRMsxGr5ekrNP6ERUmCD6_mCk7KyvlhM6GW3f93lfvlL4gvUDxaOcE-Wa9WAr8zogoX47HeWRgU1NpJYm_eFWtcnp4Nw" + }, + "refusal": "expired" +} diff --git a/contract/vectors/signed-header/machine.json b/contract/vectors/signed-header/machine.json new file mode 100644 index 0000000..20696eb --- /dev/null +++ b/contract/vectors/signed-header/machine.json @@ -0,0 +1,26 @@ +{ + "description": "A Machine Identity calling with its own credential.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "", + "X-Auth-Request-Kind": "machine", + "X-Auth-Request-Name": "", + "X-Auth-Request-Roles": "backup", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000b7", + "X-Auth-Request-Via": "machine", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOiJ3aWtpIiwiZXhwIjoxNzkyNDk3NjYwLCJpYXQiOjE3OTI0OTc2MDAsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLnRlc3QiLCJraW5kIjoibWFjaGluZSIsInJvbGVzIjpbImJhY2t1cCJdLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYjciLCJ0d29fZmFjdG9yIjpmYWxzZSwidmVyIjoiMS4xIiwidmlhIjoibWFjaGluZSJ9.LILh8lR3UI1eRoqmSafJfoakmyDKe3Z_-gO0Gqk1rBoXpUi8vBUNjuPaF1jBaNhTwPcHQsu2NAvOh99IYsTWz02gBx2m2rxLR14E5Em5H3s45TxclrVAuWjznbKh9SmKUwzn2sPR8pidtxnBGaIKHj7cIQ5Pk_d3OjC7fLxLkjjtzAdxANX8LGdR5lyFPrqwvbrLoPAnZkyWN3Tf3__YdFMA-sezxPfTtdVyr89RN5P0ZqyP6dcY1eGvY9rtGEwUpmHZDh5ff0Wp-PCaAX3CAAMF4DFN7sTpZ4KJXLJvsq1_WB0Fe8rrmVCZzsheyei677Fs404QrAt47N1J0VGBrA" + }, + "principal": { + "subject": "00000000-0000-4000-8000-0000000000b7", + "kind": "machine", + "roles": [ + "backup" + ], + "via": "machine", + "twoFactor": false, + "ver": "1.1" + } +} diff --git a/contract/vectors/signed-header/minor-version.json b/contract/vectors/signed-header/minor-version.json new file mode 100644 index 0000000..c4f0091 --- /dev/null +++ b/contract/vectors/signed-header/minor-version.json @@ -0,0 +1,30 @@ +{ + "description": "A later minor version's Principal is read, and the field the reader does not know is ignored.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "session", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOiJ3aWtpIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImZ1dHVyZSI6ImEgZmllbGQgYSBsYXRlciBtaW5vciB2ZXJzaW9uIGFkZHMiLCJpYXQiOjE3OTI0OTc2MDAsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLnRlc3QiLCJraW5kIjoidXNlciIsIm5hbWUiOiJBZGEgTG92ZWxhY2UiLCJyb2xlcyI6WyJ1c2VyIiwiZWRpdG9yIl0sInNpZCI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBjMSIsInN1YiI6IjAwMDAwMDAwLTAwMDAtNDAwMC04MDAwLTAwMDAwMDAwMDBhMSIsInR3b19mYWN0b3IiOmZhbHNlLCJ2ZXIiOiIxLjIiLCJ2aWEiOiJzZXNzaW9uIn0.KnR0Yd4_gYNX3GehQJHg-pVB3NVFTvEjS09VCq0AkBCINAMfSYHd-gKh4qUiZNwtV2M_i1pchYjQb0hB0WZEf_KJyRVIhJ5IPJ_KfSRoZrEzyr2gZOpv8Yzb0geRfcMi3LKselUEvNPJ_0BF0q9oHNT-OcpMVHyJjo6ttyizDpJH7OGIaeT_f5x0KzV7lRUFHZzLgP4hk1yekc6SaYc5fK1KsTo1QRa5OsWo3dOlnqwOtRJ6gImQxGcHZ6fTXvcOlT3fjoRbrtN6V1BH_wGDgoKFJFm3SQtUNz1ovJPgJlCNYfBjzUo6JlNOtygdEmMkKrfToyrcp8J-CkG9maOodw" + }, + "principal": { + "subject": "00000000-0000-4000-8000-0000000000a1", + "kind": "user", + "email": "ada@example.org", + "name": "Ada Lovelace", + "roles": [ + "user", + "editor" + ], + "via": "session", + "twoFactor": false, + "sid": "00000000-0000-4000-8000-0000000000c1", + "ver": "1.2" + } +} diff --git a/contract/vectors/signed-header/missing.json b/contract/vectors/signed-header/missing.json new file mode 100644 index 0000000..eeb4cb2 --- /dev/null +++ b/contract/vectors/signed-header/missing.json @@ -0,0 +1,16 @@ +{ + "description": "Plain headers without the signed Principal are refused, whatever they say.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "session" + }, + "refusal": "missing" +} diff --git a/contract/vectors/signed-header/personal-token.json b/contract/vectors/signed-header/personal-token.json new file mode 100644 index 0000000..bb0ebb2 --- /dev/null +++ b/contract/vectors/signed-header/personal-token.json @@ -0,0 +1,29 @@ +{ + "description": "A User calling with a Personal Token: no session, and no two-factor.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "personal-token", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOiJ3aWtpIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImlhdCI6MTc5MjQ5NzYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUudGVzdCIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGVzIjpbInVzZXIiLCJlZGl0b3IiXSwic3ViIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGExIiwidHdvX2ZhY3RvciI6ZmFsc2UsInZlciI6IjEuMSIsInZpYSI6InBlcnNvbmFsLXRva2VuIn0.HS0UioA6z2d8dDk9vKfFfJivTRlUagTE3Pcyo2Yzx2xNOrsysh1NKU9CHqgMygeGc7EcG_MoY14IvN28eQRef6G_uau7zfNd3d6FTkAjKT11a-mz7QtCQ_xDiRSZvdWlvZg7v5c0s98VVexRSeD2DHIlGMW95zV116B9pAed9y6zD2sNWjMm5j-xuoEPJat5gDd1bs5UNPJ_rOkMRB072PQG2FfJjVvl0gV3MKLK0U2OciankeQa6_YWqh3KnR3krHZ64cCn3t_u5p1oYr9tUwzhIXOrYq5ahW9qgUR-xSHayyZ72Bq3YdvvhDnFmw8UM7-DW1_-tFumphgTeLu4dw" + }, + "principal": { + "subject": "00000000-0000-4000-8000-0000000000a1", + "kind": "user", + "email": "ada@example.org", + "name": "Ada Lovelace", + "roles": [ + "user", + "editor" + ], + "via": "personal-token", + "twoFactor": false, + "ver": "1.1" + } +} diff --git a/contract/vectors/signed-header/session-two-factor.json b/contract/vectors/signed-header/session-two-factor.json new file mode 100644 index 0000000..7bf51c3 --- /dev/null +++ b/contract/vectors/signed-header/session-two-factor.json @@ -0,0 +1,31 @@ +{ + "description": "A User whose session proved two-factor fifteen minutes before the Gate signed.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "session", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOiJ3aWtpIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImlhdCI6MTc5MjQ5NzYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUudGVzdCIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGVzIjpbInVzZXIiLCJlZGl0b3IiXSwic2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGMxIiwic3ViIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGExIiwidHdvX2ZhY3RvciI6dHJ1ZSwidHdvX2ZhY3Rvcl9hdCI6MTc5MjQ5NjcwMCwidmVyIjoiMS4xIiwidmlhIjoic2Vzc2lvbiJ9.x-vvNNAi5AOpL9d9mg5gWL5LRsKo---qPmQrkm8Iu5gse-VAU2eN1Qa1qg22Z-LfmoKZUID9BemTFphHLM59QsgQ5bIjj83lpixnh39Ftau-jGBWRfvlPy0X7ntxIh70DF8MKviS776d_AtONXXeqz_QCt000YNh2qaV147gWBoR_e7g0y8d9C8LOdammc7wyZW7hix4I-i0tr2UqrVdf0ckkz0j-LOIT2KuYrrRN0Jd9nez3NqgiKCIL43M6Mwojr2g8PehhVXVbdtlb4P_6xh8Kmqfsgps0G2vDrm15abO-Acbxws6OV7p504IHtgKUD5G8sGbqoZnoK0NMWkGww" + }, + "principal": { + "subject": "00000000-0000-4000-8000-0000000000a1", + "kind": "user", + "email": "ada@example.org", + "name": "Ada Lovelace", + "roles": [ + "user", + "editor" + ], + "via": "session", + "twoFactor": true, + "twoFactorAt": "2026-10-20T11:45:00Z", + "sid": "00000000-0000-4000-8000-0000000000c1", + "ver": "1.1" + } +} diff --git a/contract/vectors/signed-header/session.json b/contract/vectors/signed-header/session.json new file mode 100644 index 0000000..a360165 --- /dev/null +++ b/contract/vectors/signed-header/session.json @@ -0,0 +1,30 @@ +{ + "description": "A User signed in with a Single sign-on session that never proved two-factor.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "session", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOiJ3aWtpIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImlhdCI6MTc5MjQ5NzYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUudGVzdCIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGVzIjpbInVzZXIiLCJlZGl0b3IiXSwic2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGMxIiwic3ViIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGExIiwidHdvX2ZhY3RvciI6ZmFsc2UsInZlciI6IjEuMSIsInZpYSI6InNlc3Npb24ifQ.rEbl5FwAq4eMnl41n0tMcZybbUmFBrjjeGYqRRVyqf68kAhpk7cKK6W_Krag1JiDICMWXZxXCPuaHL-28yrSBggg2V9m_g-qh3VEAjVxzTh-_jbkErtyqW9Wl6UTbqPNhrFEU4-jffWqOtl-NPJGh_XmcvtCbM3RUYAC9Hkm-oX8ZFMPqPpoPn4l7kvlFUfq5solAN3VLYw2Z8Qv-pgPt7O5K_dn_xh9f1rWGYqx-VmNUoitvdD7GSXXV1zRMsxGr5ekrNP6ERUmCD6_mCk7KyvlhM6GW3f93lfvlL4gvUDxaOcE-Wa9WAr8zogoX47HeWRgU1NpJYm_eFWtcnp4Nw" + }, + "principal": { + "subject": "00000000-0000-4000-8000-0000000000a1", + "kind": "user", + "email": "ada@example.org", + "name": "Ada Lovelace", + "roles": [ + "user", + "editor" + ], + "via": "session", + "twoFactor": false, + "sid": "00000000-0000-4000-8000-0000000000c1", + "ver": "1.1" + } +} diff --git a/contract/vectors/signed-header/spoofed-plain-headers.json b/contract/vectors/signed-header/spoofed-plain-headers.json new file mode 100644 index 0000000..8e58946 --- /dev/null +++ b/contract/vectors/signed-header/spoofed-plain-headers.json @@ -0,0 +1,30 @@ +{ + "description": "A caller's own plain headers never stand in for the signed Principal.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "admin", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000e1", + "X-Auth-Request-Via": "session", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOiJ3aWtpIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImlhdCI6MTc5MjQ5NzYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUudGVzdCIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGVzIjpbInVzZXIiLCJlZGl0b3IiXSwic2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGMxIiwic3ViIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGExIiwidHdvX2ZhY3RvciI6ZmFsc2UsInZlciI6IjEuMSIsInZpYSI6InNlc3Npb24ifQ.rEbl5FwAq4eMnl41n0tMcZybbUmFBrjjeGYqRRVyqf68kAhpk7cKK6W_Krag1JiDICMWXZxXCPuaHL-28yrSBggg2V9m_g-qh3VEAjVxzTh-_jbkErtyqW9Wl6UTbqPNhrFEU4-jffWqOtl-NPJGh_XmcvtCbM3RUYAC9Hkm-oX8ZFMPqPpoPn4l7kvlFUfq5solAN3VLYw2Z8Qv-pgPt7O5K_dn_xh9f1rWGYqx-VmNUoitvdD7GSXXV1zRMsxGr5ekrNP6ERUmCD6_mCk7KyvlhM6GW3f93lfvlL4gvUDxaOcE-Wa9WAr8zogoX47HeWRgU1NpJYm_eFWtcnp4Nw" + }, + "principal": { + "subject": "00000000-0000-4000-8000-0000000000a1", + "kind": "user", + "email": "ada@example.org", + "name": "Ada Lovelace", + "roles": [ + "user", + "editor" + ], + "via": "session", + "twoFactor": false, + "sid": "00000000-0000-4000-8000-0000000000c1", + "ver": "1.1" + } +} diff --git a/contract/vectors/signed-header/tool.json b/contract/vectors/signed-header/tool.json new file mode 100644 index 0000000..66262fd --- /dev/null +++ b/contract/vectors/signed-header/tool.json @@ -0,0 +1,33 @@ +{ + "description": "A tool acting for a User, named as the actor.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "Claude Code", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "tool", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhY3QiOnsibmFtZSI6IkNsYXVkZSBDb2RlIiwic3ViIjoibWNwXzZmMWQwYzJlIn0sImF1ZCI6Indpa2kiLCJlbWFpbCI6ImFkYUBleGFtcGxlLm9yZyIsImV4cCI6MTc5MjQ5NzY2MCwiaWF0IjoxNzkyNDk3NjAwLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS50ZXN0Iiwia2luZCI6InVzZXIiLCJuYW1lIjoiQWRhIExvdmVsYWNlIiwicm9sZXMiOlsidXNlciIsImVkaXRvciJdLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYTEiLCJ0d29fZmFjdG9yIjpmYWxzZSwidmVyIjoiMS4xIiwidmlhIjoidG9vbCJ9.0JcmFCUTnLrf0zkxq5dmSThBLYypBuoUwH3aFycMcjQt8nId2V-lrWveABIVI2ZSIRvcZBQg7AX76CMere6vbpTkUB8YCgSbvHW8eSlwIuTPiRSC5SW6Djdezg6y6boHpMOnDTR_kABM1Dfu9bXgPrpSL496sINRGGDCOLD7Q8h8bIePiGLYs3_UuQf-kMDoo9WTVklPoUQgrpmsec7iPZJvd6x6S4Opvww7udhPb3lRxS1ahk17BCXzOPw4gXB43-R7TSKnUXJ1u-foiwN7o1CL22owPAwRQpnKrAJiy3zmrXeudsXusJV6Dpx6V6Qt92ysWs2JxJwiPeBNF0giCA" + }, + "principal": { + "subject": "00000000-0000-4000-8000-0000000000a1", + "kind": "user", + "email": "ada@example.org", + "name": "Ada Lovelace", + "roles": [ + "user", + "editor" + ], + "via": "tool", + "actor": { + "id": "mcp_6f1d0c2e", + "name": "Claude Code" + }, + "twoFactor": false, + "ver": "1.1" + } +} diff --git a/contract/vectors/signed-header/unknown-key.json b/contract/vectors/signed-header/unknown-key.json new file mode 100644 index 0000000..562f165 --- /dev/null +++ b/contract/vectors/signed-header/unknown-key.json @@ -0,0 +1,17 @@ +{ + "description": "A Principal signed by a key the Issuer's JWKS does not publish is refused.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "session", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1zdHJhbmdlciIsInR5cCI6ImlkZW50aXR5LXByaW5jaXBhbCtqd3QifQ.eyJhdWQiOiJ3aWtpIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImlhdCI6MTc5MjQ5NzYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUudGVzdCIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGVzIjpbInVzZXIiLCJlZGl0b3IiXSwic2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGMxIiwic3ViIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGExIiwidHdvX2ZhY3RvciI6ZmFsc2UsInZlciI6IjEuMSIsInZpYSI6InNlc3Npb24ifQ.ax1-JrMNu-MzpF5dnKZXxPJp5H13gbsyIq_-R5FhwXtKvZKU-PiIQiRQPvZQvm9fw1UF0cidwM4ygFj9OAl2L2pNjWh5_OhpJDbOT7r1qWokMNxVWpJvLn3kFFcJWP5cYEiaL4sqmJKzXJJZ_pWkuZLUxScoGU-stRzaOuuW2KFmtm0l_P75Hexfa40SJawZc3NKEj_6rREZOm1N7WOjeViY6rcrmZw7F74SkV4nxkgk9j9Zm_h8RvPgPU6-tVdfa2GS-DyDRy56EDXexGM07XpqD7b_X8Ip-UuhzOG76UG_Qg33Nt_ZwSrzA8J9bOoRAeht8KsWiwlb_rCLZHdf8g" + }, + "refusal": "unknown-key" +} diff --git a/contract/vectors/signed-header/unknown-major.json b/contract/vectors/signed-header/unknown-major.json new file mode 100644 index 0000000..58aad29 --- /dev/null +++ b/contract/vectors/signed-header/unknown-major.json @@ -0,0 +1,17 @@ +{ + "description": "A Principal stated in a major version the reader does not know is refused.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "session", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOiJ3aWtpIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImlhdCI6MTc5MjQ5NzYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUudGVzdCIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGVzIjpbInVzZXIiLCJlZGl0b3IiXSwic2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGMxIiwic3ViIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGExIiwidHdvX2ZhY3RvciI6ZmFsc2UsInZlciI6IjIuMCIsInZpYSI6InNlc3Npb24ifQ.QmEOJSYEEwLA0o5I4Jybcr3-a83CITv5dnAjN7do_PhOgdSQQIbwLjxr_L3Sii5_GIoabCwTYLWeB-_Q_9eYHdnzq21Ogjkz6iv1j8gW2YcMM5K3H6CJFsILAMSAsbc-EBHd72Spf4yTsFgRZdKxAdmDM0MkIhRteB3n-kXVeMK7nBpfZ-mrYDrFebClZl-B9CPiia_hyMzj9xGNWPc2gqfAYFPnAf4c06LWWdP2alNQLDWEvuh8bQrVSR2uE16peX2m-t46ZYH8zhScTL64CdBQjuLJnZcjN7zCfVJjM-D1bCi3-C5c0x7RiMEhFcC3ERYYz5TKkpHjLbZz5WZU8g" + }, + "refusal": "unknown-major" +} diff --git a/contract/vectors/signed-header/wrong-audience.json b/contract/vectors/signed-header/wrong-audience.json new file mode 100644 index 0000000..2d08f29 --- /dev/null +++ b/contract/vectors/signed-header/wrong-audience.json @@ -0,0 +1,17 @@ +{ + "description": "A Principal signed for another Client is refused.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "session", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOiJncmFmYW5hIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImlhdCI6MTc5MjQ5NzYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUudGVzdCIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGVzIjpbInVzZXIiLCJlZGl0b3IiXSwic2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGMxIiwic3ViIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGExIiwidHdvX2ZhY3RvciI6ZmFsc2UsInZlciI6IjEuMSIsInZpYSI6InNlc3Npb24ifQ.GBCGlV5nllPbUaMqcitEiwb6NMcQjlskGwVqsmLvt9Q76k-enYuoJ_qyM9S-vHLQKWuyCp56dapCFkEt8MQ13bhiFsXoe_zt50RtWNR3sugFujHHLbsSbcJ3atdGQZ2MSrtIB52JHURSrFM2mjs2T3TaOjfVGpJuUjjAWcJxSzREK22JC10h3ML7xl5mZ4Vz4oQRxaxfBinMo1gYS-uQaX_UMCbFb-X8oEwqxUAqvahlN4oCXfIgjzHWeBmrVtbLYq0rkpik3Ecyx5VLVxOJyUzYTAj7dED7i0-83-cJxIKzMnUjdJM4YddkuVgt9jsZC60pZj9In-Yn4aj2Wu0gzA" + }, + "refusal": "wrong-audience" +} diff --git a/contract/vectors/signed-header/wrong-issuer.json b/contract/vectors/signed-header/wrong-issuer.json new file mode 100644 index 0000000..17b33e8 --- /dev/null +++ b/contract/vectors/signed-header/wrong-issuer.json @@ -0,0 +1,17 @@ +{ + "description": "A Principal another issuer states is refused.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "session", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJpZGVudGl0eS1wcmluY2lwYWwrand0In0.eyJhdWQiOiJ3aWtpIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImlhdCI6MTc5MjQ5NzYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUub3JnIiwia2luZCI6InVzZXIiLCJuYW1lIjoiQWRhIExvdmVsYWNlIiwicm9sZXMiOlsidXNlciIsImVkaXRvciJdLCJzaWQiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYzEiLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTQwMDAtODAwMC0wMDAwMDAwMDAwYTEiLCJ0d29fZmFjdG9yIjpmYWxzZSwidmVyIjoiMS4xIiwidmlhIjoic2Vzc2lvbiJ9.J2dO_4wRZtgBkGOcEQNKm-rJOMu1Ce1MTt1e_b17WB5LqYFfw8THBholxOxrCaz_QzGjcXa7XPenQmrY3JaJvD2z6j1QhxumnIoyBti75RmRF9W8RyFUJdJASIa9xtn6tNIGnO6ItkDBYyAD-KoR5K1Zrb2AW3cpQv0P-DcxUqxknxMHQgZzK1C0URbr1Hm8vkuvgVaBi3V--21l9sfn008vmqZj0qcSVUeQRC7WWKddntyNR0NDHz8qRCRR6H1_H_ot-25NzthQo64Zpjqvh9XMA5jolsaVXfj78lQDPm5OWIxrwl65ltZWaID12sXU0tu5zGJ1RMEf701uELMkqw" + }, + "refusal": "wrong-issuer" +} diff --git a/contract/vectors/signed-header/wrong-typ.json b/contract/vectors/signed-header/wrong-typ.json new file mode 100644 index 0000000..de8ade6 --- /dev/null +++ b/contract/vectors/signed-header/wrong-typ.json @@ -0,0 +1,17 @@ +{ + "description": "A JWT typed as anything but identity-principal+jwt, such as an access token, is refused.", + "issuer": "https://auth.example.test", + "client": "wiki", + "now": "2026-10-20T12:00:30Z", + "headers": { + "X-Auth-Request-Actor": "", + "X-Auth-Request-Email": "ada@example.org", + "X-Auth-Request-Kind": "user", + "X-Auth-Request-Name": "Ada Lovelace", + "X-Auth-Request-Roles": "user,editor", + "X-Auth-Request-User": "00000000-0000-4000-8000-0000000000a1", + "X-Auth-Request-Via": "session", + "X-Identity-Principal": "eyJhbGciOiJSUzI1NiIsImtpZCI6InZlY3Rvci1pc3N1ZXIiLCJ0eXAiOiJhdCtqd3QifQ.eyJhdWQiOiJ3aWtpIiwiZW1haWwiOiJhZGFAZXhhbXBsZS5vcmciLCJleHAiOjE3OTI0OTc2NjAsImlhdCI6MTc5MjQ5NzYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUudGVzdCIsImtpbmQiOiJ1c2VyIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGVzIjpbInVzZXIiLCJlZGl0b3IiXSwic2lkIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGMxIiwic3ViIjoiMDAwMDAwMDAtMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMGExIiwidHdvX2ZhY3RvciI6ZmFsc2UsInZlciI6IjEuMSIsInZpYSI6InNlc3Npb24ifQ.OpaUgb3R-FvbbQL3N0cXYec_CFsL4-6JUC8D93poMV-JSGhe7XCWM-gMnxSR9TSIzLa4siryXqyEuj8Tcgr92PvBstIOzdKN5p4iqfPnS0onU4EuvwhjAUwwhGU3Ii0pghT5YUsVziqbOr1lSKp9XKnn8TOjI0Np8iQhPTLGa7QrHKP6XL3RtpEmzn-8D1WjJbRPbfOItLU_eG-aRe0UWmG2CBUQ7CVtW_wZpKPU26K61RBfTxLw-pho34yLh_WtjtE7GcnWWDrsZrtA1huqdOt1I7rqX98EE4vK_i6gL68KFOyIb2BpKVvJ2aYPJFnMb53LA9YdnKTM1LNInQWB4Q" + }, + "refusal": "wrong-typ" +} diff --git a/mise.toml b/mise.toml index b2e90b6..e5b5f38 100644 --- a/mise.toml +++ b/mise.toml @@ -1,5 +1,7 @@ # Toolchain for local work and CI (jdx/mise-action). `mise install` is the only setup step. [tools] +python = "3.13.16" +uv = "0.13.1" task = "3.53.1" actionlint = "1.7.12" gitleaks = "8.30.1" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..30cb889 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[project] +name = "jorisjonkers-commons" +version = "0.0.0" +description = "Shared Python packages for estate services." +readme = "README.md" +requires-python = ">=3.11" +license = "LicenseRef-JorisJonkers-Proprietary-1.0" +license-files = ["LICENSE"] +dependencies = [ + "cryptography>=44", +] + +[dependency-groups] +dev = [ + "mypy", + "pytest", + "pytest-cov", + "ruff", +] + +[build-system] +requires = ["hatchling>=1.27"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/jorisjonkers_commons"] + +[tool.pytest.ini_options] +addopts = ["--strict-markers", "-p", "no:cacheprovider"] +testpaths = ["tests"] + +[tool.coverage.run] +branch = true +source = ["jorisjonkers_commons"] + +[tool.coverage.report] +fail_under = 95 +show_missing = true + +[tool.ruff] +line-length = 120 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "B", "UP", "SIM", "RUF"] + +[tool.mypy] +python_version = "3.11" +strict = true +files = ["src", "tests", "scripts"] diff --git a/scripts/generate_principal.py b/scripts/generate_principal.py new file mode 100644 index 0000000..efc3e09 --- /dev/null +++ b/scripts/generate_principal.py @@ -0,0 +1,94 @@ +"""Writes the Python type of identity's Principal contract from its JSON Schema, so a field the +contract adds reaches the adapter mechanically. It knows the shapes the schema uses: strings (with +an enum, or as a date-time read as a datetime), booleans, arrays of strings, and objects. + + python scripts/generate_principal.py # rewrites src/jorisjonkers_commons/identity/principal.py +""" + +import json +import re +import sys +import textwrap +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parent.parent +SCHEMA = ROOT / "contract" / "principal.schema.json" +GENERATED = ROOT / "src" / "jorisjonkers_commons" / "identity" / "principal.py" + +HEADER = '''# Generated by scripts/generate_principal.py from the Principal contract's schema; do not edit. +"""The Principal identity states on every channel, as the pinned contract's schema describes it.""" + +from dataclasses import dataclass +from datetime import datetime +from typing import Literal +''' + + +def _snake(name: str) -> str: + return re.sub(r"(? str: + return name[:1].upper() + name[1:] + + +def _comment(description: str | None, indent: str) -> str: + if not description: + return "" + prefix = f"{indent}#: " + lines = textwrap.wrap(description, 100, initial_indent=prefix, subsequent_indent=prefix) + return "".join(f"{line}\n" for line in lines) + + +def _docstring(description: str | None) -> str: + if not description: + return "" + lines = textwrap.wrap(description, 96) + if len(lines) == 1: + return f' """{lines[0]}"""\n\n' + body = "".join(f" {line}\n" for line in lines) + return f' """{body.lstrip()} """\n\n' + + +def _type_of(name: str, field: dict[str, Any], types: list[str]) -> str: + if "enum" in field: + values = ", ".join(f'"{v}"' for v in sorted(field["enum"])) + types.append(f"{_comment(field.get('description'), '')}{_pascal(name)} = Literal[{values}]\n") + return _pascal(name) + kind = field.get("type") + if kind == "string": + return "datetime" if field.get("format") == "date-time" else "str" + if kind == "boolean": + return "bool" + if kind == "array" and field.get("items", {}).get("type") == "string": + return "tuple[str, ...]" + if kind == "object": + types.append(_dataclass(_pascal(name), field, types)) + return _pascal(name) + raise ValueError(f'no Python type for {name} (type "{kind}")') + + +def _dataclass(name: str, schema: dict[str, Any], types: list[str]) -> str: + required = set(schema.get("required", [])) + fields = schema.get("properties", {}) + ordered = [f for f in fields if f in required] + [f for f in fields if f not in required] + lines = [] + for field in ordered: + spec = fields[field] + annotation = _type_of(field, spec, types) + default = "" if field in required else " | None = None" + lines.append(f"{_comment(spec.get('description'), ' ')} {_snake(field)}: {annotation}{default}\n") + return f"@dataclass(frozen=True)\nclass {name}:\n{_docstring(schema.get('description'))}{''.join(lines)}" + + +def generate(schema: dict[str, Any]) -> str: + """The Python source of the Principal type, from the contract's schema.""" + types: list[str] = [] + principal = _dataclass(schema.get("title", "Principal"), schema, types) + return HEADER + "\n" + "\n\n".join([*types, principal]) + + +if __name__ == "__main__": # pragma: no cover + GENERATED.write_text(generate(json.loads(SCHEMA.read_text()))) + sys.stdout.write(f"wrote {GENERATED.relative_to(ROOT)}\n") diff --git a/scripts/pin-contract.sh b/scripts/pin-contract.sh new file mode 100755 index 0000000..e49ba57 --- /dev/null +++ b/scripts/pin-contract.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Vendors identity's Principal contract at a ref (a tag such as contract/principal/v1.1.0, or a +# commit) into contract/, which the conformance tests read and the Principal type is generated from. +set -euo pipefail + +ref=${1:?usage: pin-contract.sh } +dest="$(cd "$(dirname "$0")/.." && pwd)/contract" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +curl -fsSL "https://codeload.github.com/JorisJonkers-dev/identity/tar.gz/$ref" | tar -xz -C "$tmp" +src=$(find "$tmp" -maxdepth 3 -type d -path '*/contract/principal' | head -1) +rm -rf "$dest" +mkdir -p "$dest" +cp -R "$src/principal.schema.json" "$src/mapping.json" "$src/vectors" "$dest/" +echo "$ref" >"$dest/REF" +echo "pinned the Principal contract at $ref" diff --git a/src/jorisjonkers_commons/__init__.py b/src/jorisjonkers_commons/__init__.py new file mode 100644 index 0000000..e16f393 --- /dev/null +++ b/src/jorisjonkers_commons/__init__.py @@ -0,0 +1 @@ +"""Shared Python packages for estate services.""" diff --git a/src/jorisjonkers_commons/identity/__init__.py b/src/jorisjonkers_commons/identity/__init__.py new file mode 100644 index 0000000..cc888dd --- /dev/null +++ b/src/jorisjonkers_commons/identity/__init__.py @@ -0,0 +1,53 @@ +"""Reads who is calling an app from identity's Principal contract +(github.com/JorisJonkers-dev/identity/contract/principal), and guards its routes with the result.""" + +from .asgi import ( + STEP_UP_PATH, + principal_middleware, + principal_of, + refuse_tools, + require_machine, + require_recent_two_factor, + require_role, + require_user, +) +from .principal import Actor, Kind, Principal, Via +from .reader import ( + HEADER, + MAJOR, + TYPE, + Fetch, + Headers, + IdentityUnavailable, + PrincipalReader, + PrincipalRefused, + Request, + Response, + urllib_fetch, +) + +__all__ = [ + "HEADER", + "MAJOR", + "STEP_UP_PATH", + "TYPE", + "Actor", + "Fetch", + "Headers", + "IdentityUnavailable", + "Kind", + "Principal", + "PrincipalReader", + "PrincipalRefused", + "Request", + "Response", + "Via", + "principal_middleware", + "principal_of", + "refuse_tools", + "require_machine", + "require_recent_two_factor", + "require_role", + "require_user", + "urllib_fetch", +] diff --git a/src/jorisjonkers_commons/identity/asgi.py b/src/jorisjonkers_commons/identity/asgi.py new file mode 100644 index 0000000..c317db4 --- /dev/null +++ b/src/jorisjonkers_commons/identity/asgi.py @@ -0,0 +1,146 @@ +"""The Principal as ASGI middleware, for Starlette, FastAPI or any ASGI app: principal_middleware +reads each request's Principal, and the policy helpers guard the routes they wrap.""" + +import asyncio +import urllib.parse +from collections.abc import Awaitable, Callable, MutableMapping +from datetime import UTC, datetime, timedelta +from typing import Any + +from .principal import Principal +from .reader import IdentityUnavailable, PrincipalReader, PrincipalRefused + +Scope = MutableMapping[str, Any] +Message = MutableMapping[str, Any] +Receive = Callable[[], Awaitable[Message]] +Send = Callable[[Message], Awaitable[None]] +ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]] +Policy = Callable[[ASGIApp], ASGIApp] + +#: The Gate's step-up on the app's own host, which proves two-factor again and comes back. +STEP_UP_PATH = "/.auth/step-up" + +_REQUESTS = ("http", "websocket") +# A WebSocket refused before it is accepted closes with 1008, which the server answers as 403. +_POLICY_VIOLATION = 1008 + + +def principal_of(scope: Scope) -> Principal | None: + """The Principal principal_middleware read for the request, if any; Starlette's + request.state.principal reads the same.""" + principal = scope.get("state", {}).get("principal") + return principal if isinstance(principal, Principal) else None + + +def principal_middleware(app: ASGIApp, reader: PrincipalReader) -> ASGIApp: + """Reads each request's Principal and passes it on in the request's state. A request whose + Principal is refused is answered 401, and one identity could not be asked about 503.""" + + async def middleware(scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] not in _REQUESTS: + await app(scope, receive, send) + return + headers = _headers(scope) + try: + principal = await asyncio.to_thread(reader.read, lambda name: headers.get(name.lower())) + except PrincipalRefused: + await _answer(scope, send, 401) + return + except IdentityUnavailable: + await _answer(scope, send, 503) + return + await app({**scope, "state": {**scope.get("state", {}), "principal": principal}}, receive, send) + + return middleware + + +def _guard(allow: Callable[[Principal], bool]) -> Policy: + """Lets a request through when allow says so of its Principal, answers 403 when it does not, + and 401 when the request carries none.""" + + def policy(app: ASGIApp) -> ASGIApp: + async def guarded(scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] not in _REQUESTS: + await app(scope, receive, send) + return + principal = principal_of(scope) + if principal is None: + await _answer(scope, send, 401) + elif not allow(principal): + await _answer(scope, send, 403) + else: + await app(scope, receive, send) + + return guarded + + return policy + + +def require_role(role: str) -> Policy: + """Lets through a caller holding role on the app's Client. It takes the str-typed App Roles + deploy-kit generates as readily as a plain string.""" + return _guard(lambda p: role in p.roles) + + +def refuse_tools() -> Policy: + """Turns away a tool acting for a User, so a tool can read but never, say, delete.""" + return _guard(lambda p: p.via != "tool") + + +def require_user() -> Policy: + """Lets through only a person, never a Machine Identity.""" + return _guard(lambda p: p.kind == "user") + + +def require_machine() -> Policy: + """Lets through only a Machine Identity, never a person.""" + return _guard(lambda p: p.kind == "machine") + + +def require_recent_two_factor(minutes: int, now: Callable[[], datetime] = lambda: datetime.now(UTC)) -> Policy: + """Lets through a session that proved two-factor within minutes. A session whose proof is older, + or that has none, is sent to the Gate's step-up on the app's own host and comes back to the same + page; a Personal Token, a tool or a machine, which no browser carries through a step-up, is + answered 403.""" + window = timedelta(minutes=minutes) + + def policy(app: ASGIApp) -> ASGIApp: + async def guarded(scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] not in _REQUESTS: + await app(scope, receive, send) + return + principal = principal_of(scope) + if principal is None: + await _answer(scope, send, 401) + elif principal.two_factor_at is not None and now() - principal.two_factor_at <= window: + await app(scope, receive, send) + elif principal.via == "session": + query = urllib.parse.urlencode({"minutes": minutes, "rd": _request_uri(scope)}) + await _answer(scope, send, 303, f"{STEP_UP_PATH}?{query}") + else: + await _answer(scope, send, 403) + + return guarded + + return policy + + +def _headers(scope: Scope) -> dict[str, str]: + return {name.decode("latin-1").lower(): value.decode("latin-1") for name, value in scope.get("headers", [])} + + +def _request_uri(scope: Scope) -> str: + path = str(scope.get("path") or "/") + query = bytes(scope.get("query_string") or b"").decode("latin-1") + return f"{path}?{query}" if query else path + + +async def _answer(scope: Scope, send: Send, status: int, location: str | None = None) -> None: + if scope["type"] == "websocket": + await send({"type": "websocket.close", "code": _POLICY_VIOLATION}) + return + headers = [(b"content-length", b"0")] + if location is not None: + headers.append((b"location", location.encode("latin-1"))) + await send({"type": "http.response.start", "status": status, "headers": headers}) + await send({"type": "http.response.body", "body": b""}) diff --git a/src/jorisjonkers_commons/identity/principal.py b/src/jorisjonkers_commons/identity/principal.py new file mode 100644 index 0000000..8778807 --- /dev/null +++ b/src/jorisjonkers_commons/identity/principal.py @@ -0,0 +1,56 @@ +# Generated by scripts/generate_principal.py from the Principal contract's schema; do not edit. +"""The Principal identity states on every channel, as the pinned contract's schema describes it.""" + +from dataclasses import dataclass +from datetime import datetime +from typing import Literal + +#: Whether a person or a workload is calling. +Kind = Literal["machine", "user"] + + +#: How the caller signed in: a Single sign-on session, a Personal Token, a tool acting for the User, +#: or a Machine Identity's own credential. +Via = Literal["machine", "personal-token", "session", "tool"] + + +@dataclass(frozen=True) +class Actor: + """For via tool, the tool acting for the User: its registration's client id and the name it + registered with. + """ + + id: str + name: str + + +@dataclass(frozen=True) +class Principal: + """Who is calling an application, as identity states it on every channel. Contract major version 1; + a minor version only adds optional fields, which a reader ignores when it does not know them. + """ + + #: The User's or Machine Identity's id. + subject: str + #: Whether a person or a workload is calling. + kind: Kind + #: The App Roles the caller holds on the Client the request is for, and no other Client's. + roles: tuple[str, ...] + #: How the caller signed in: a Single sign-on session, a Personal Token, a tool acting for the + #: User, or a Machine Identity's own credential. + via: Via + #: Whether the session proved two-factor. + two_factor: bool + #: The contract version the Principal was stated in, as major.minor. + ver: str + #: The User's sign-in email. + email: str | None = None + #: The User's name. + name: str | None = None + #: For via tool, the tool acting for the User: its registration's client id and the name it + #: registered with. + actor: Actor | None = None + #: When the session last proved two-factor. + two_factor_at: datetime | None = None + #: The Single sign-on session, for via session. + sid: str | None = None diff --git a/src/jorisjonkers_commons/identity/reader.py b/src/jorisjonkers_commons/identity/reader.py new file mode 100644 index 0000000..1b09ca5 --- /dev/null +++ b/src/jorisjonkers_commons/identity/reader.py @@ -0,0 +1,308 @@ +"""Reads who is calling an app from identity's Principal contract: the Gate's signed header, verified +against the Issuer's published keys, never the plain X-Auth-Request-* headers beside it.""" + +import base64 +import binascii +import hashlib +import json +import threading +import urllib.error +import urllib.parse +import urllib.request +from collections.abc import Callable, Mapping +from dataclasses import dataclass, field +from datetime import UTC, datetime, timedelta +from typing import Any, cast + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey, RSAPublicNumbers +from cryptography.hazmat.primitives.hashes import SHA256 + +from .principal import Actor, Kind, Principal, Via + +#: The Gate's signed Principal. +HEADER = "X-Identity-Principal" +#: The signed Principal's JWT typ, which no access token carries. +TYPE = "identity-principal+jwt" +#: The contract major version this package reads. +MAJOR = "1" + +# zitadel/oidc signs access and ID tokens alike as JWT; an access token alone names a client_id. +_ACCESS_TOKEN_TYPE = "JWT" +_REFETCH_AFTER = timedelta(minutes=1) +_ANSWER_LIFETIME = timedelta(seconds=30) +_MAX_ANSWERS = 10_000 +_KINDS = ("user", "machine") +_VIAS = ("session", "personal-token", "tool", "machine") + + +class PrincipalRefused(Exception): + """Why a request carries no Principal. reason is one of the contract's refusals: missing, + wrong-typ, wrong-issuer, wrong-audience, expired, unknown-key, bad-signature, unknown-major, + not-an-access-token or inactive.""" + + def __init__(self, reason: str) -> None: + super().__init__(f"the request's Principal is refused: {reason}") + self.reason = reason + + +class IdentityUnavailable(Exception): + """identity could not be asked: its keys or its introspection did not answer.""" + + +@dataclass(frozen=True) +class Request: + """A request the reader sends identity.""" + + method: str + url: str + headers: Mapping[str, str] = field(default_factory=dict) + body: bytes | None = None + + +@dataclass(frozen=True) +class Response: + """identity's answer to a Request.""" + + status: int + body: bytes + + +#: Sends a Request to identity; raises IdentityUnavailable when identity does not answer. +Fetch = Callable[[Request], Response] + +#: A request's header by name, or None when it has none. +Headers = Callable[[str], str | None] + + +def urllib_fetch(request: Request, timeout: float = 5.0) -> Response: + """Sends request with urllib, the default Fetch.""" + sent = urllib.request.Request(request.url, request.body, dict(request.headers), method=request.method) + try: + with urllib.request.urlopen(sent, timeout=timeout) as answer: + return Response(answer.status, answer.read()) + except urllib.error.HTTPError as answer: + return Response(answer.code, answer.read()) + except OSError as e: + raise IdentityUnavailable(f"identity did not answer {request.url}") from e + + +@dataclass(frozen=True) +class _Answer: + principal: Principal | None + refusal: str | None + until: datetime + + +class PrincipalReader: + """Reads Principals for one Client: the app's own, which each Principal must be stated for.""" + + def __init__( + self, + issuer: str, + client_id: str, + *, + client_secret: str | None = None, + keys_url: str | None = None, + introspect_url: str | None = None, + fetch: Fetch = urllib_fetch, + now: Callable[[], datetime] = lambda: datetime.now(UTC), + ) -> None: + """issuer is identity's URL as each Principal's iss states it. An app without client_secret + refuses an opaque token as inactive.""" + if not issuer or not client_id: + raise ValueError("a PrincipalReader names its Issuer and its Client") + base = issuer.rstrip("/") + self._issuer = issuer + self._client_id = client_id + self._client_secret = client_secret + self._keys_url = keys_url or f"{base}/oidc/keys" + self._introspect_url = introspect_url or f"{base}/oidc/introspect" + self._fetch = fetch + self._now = now + self._keys_lock = threading.Lock() + self._keys: dict[str, RSAPublicKey] = {} + self._fetched: datetime | None = None + self._answers_lock = threading.Lock() + self._answers: dict[bytes, _Answer] = {} + + def read(self, header: Headers) -> Principal: + """The Principal a request carries: the Gate's signed header when there is one, or else its + bearer token, a JWT access token verified here or an opaque token introspected. Raises + PrincipalRefused saying why it carries none, and IdentityUnavailable when identity could not + be asked.""" + signed = header(HEADER) + if signed: + return self._checked(self._verified(signed, TYPE)) + scheme, _, token = (header("Authorization") or "").partition(" ") + if scheme.lower() != "bearer" or not token: + raise PrincipalRefused("missing") + if token.count(".") == 2: + claims = self._verified(token, _ACCESS_TOKEN_TYPE) + if not claims.get("client_id"): + raise PrincipalRefused("not-an-access-token") + return self._checked(claims) + return self._introspected(token) + + def _verified(self, token: str, typ: str) -> dict[str, Any]: + """The claims of a JWT of typ, signed by one of the Issuer's published keys.""" + try: + head, payload, signature = token.split(".") + protected = json.loads(_unbase64(head)) + if not isinstance(protected, dict) or protected.get("alg") != "RS256": + raise PrincipalRefused("bad-signature") + if protected.get("typ") != typ: + raise PrincipalRefused("wrong-typ") + key = self._key(protected.get("kid")) + key.verify(_unbase64(signature), f"{head}.{payload}".encode(), PKCS1v15(), SHA256()) + claims = json.loads(_unbase64(payload)) + except (ValueError, binascii.Error, InvalidSignature) as e: + raise PrincipalRefused("bad-signature") from e + if not isinstance(claims, dict): + raise PrincipalRefused("bad-signature") + return claims + + def _checked(self, claims: dict[str, Any]) -> Principal: + """claims' Principal, or why claims state none for this reader's Client.""" + exp = claims.get("exp") + if _major(claims) != MAJOR: + raise PrincipalRefused("unknown-major") + if claims.get("iss") != self._issuer: + raise PrincipalRefused("wrong-issuer") + if _audience(claims.get("aud")) != [self._client_id]: + raise PrincipalRefused("wrong-audience") + if not isinstance(exp, int | float) or self._now() >= datetime.fromtimestamp(exp, UTC): + raise PrincipalRefused("expired") + return _principal_of(claims) + + def _key(self, kid: object) -> RSAPublicKey: + """The Issuer's published key with id kid. An id the reader does not know fetches the keys + again, at most once a minute, so a rotated key is picked up and a forged id costs little.""" + if not isinstance(kid, str): + raise PrincipalRefused("unknown-key") + with self._keys_lock: + if kid in self._keys: + return self._keys[kid] + if self._fetched is not None and self._now() - self._fetched < _REFETCH_AFTER: + raise PrincipalRefused("unknown-key") + self._keys = self._published_keys() + self._fetched = self._now() + if kid in self._keys: + return self._keys[kid] + raise PrincipalRefused("unknown-key") + + def _published_keys(self) -> dict[str, RSAPublicKey]: + answer = self._fetch(Request("GET", self._keys_url)) + if answer.status != 200: + raise IdentityUnavailable(f"the Issuer's keys answered {answer.status}") + try: + keys = json.loads(answer.body).get("keys") or [] + return { + k["kid"]: RSAPublicNumbers(_integer(k["e"]), _integer(k["n"])).public_key() + for k in keys + if k.get("kty") == "RSA" and "kid" in k + } + except (ValueError, AttributeError, KeyError, TypeError) as e: + raise IdentityUnavailable("the Issuer's keys are no JWKS") from e + + def _introspected(self, token: str) -> Principal: + """The Principal identity answers for an opaque token, a Personal Token or a Connected App's, + asked at most once per thirty seconds so a revoked token stops working within them.""" + if not self._client_secret: + raise PrincipalRefused("inactive") + key = hashlib.sha256(token.encode()).digest() + now = self._now() + with self._answers_lock: + kept = self._answers.get(key) + if kept is None or now >= kept.until: + claims = self._ask(token) + try: + kept = _Answer(self._answered(claims), None, now + _ANSWER_LIFETIME) + except PrincipalRefused as refused: + kept = _Answer(None, refused.reason, now + _ANSWER_LIFETIME) + with self._answers_lock: + if len(self._answers) >= _MAX_ANSWERS: + self._answers = {} + self._answers[key] = kept + if kept.principal is None: + raise PrincipalRefused(cast(str, kept.refusal)) + return kept.principal + + def _ask(self, token: str) -> dict[str, Any]: + """identity's introspection answer about token (RFC 7662), the reader authenticating as its + own Client.""" + secret = cast(str, self._client_secret) + credentials = f"{urllib.parse.quote_plus(self._client_id)}:{urllib.parse.quote_plus(secret)}" + answer = self._fetch( + Request( + "POST", + self._introspect_url, + { + "Authorization": f"Basic {base64.b64encode(credentials.encode()).decode()}", + "Content-Type": "application/x-www-form-urlencoded", + }, + urllib.parse.urlencode({"token": token}).encode(), + ) + ) + if answer.status != 200: + raise IdentityUnavailable(f"introspection answered {answer.status}") + try: + claims = json.loads(answer.body) + except ValueError as e: + raise IdentityUnavailable("introspection answered no JSON") from e + if not isinstance(claims, dict): + raise IdentityUnavailable("introspection answered no JSON object") + return claims + + def _answered(self, claims: dict[str, Any]) -> Principal: + """The Principal an introspection answer states for this reader's Client.""" + if claims.get("active") is not True: + raise PrincipalRefused("inactive") + if _major(claims) != MAJOR: + raise PrincipalRefused("unknown-major") + if claims.get("client_id") != self._client_id or _audience(claims.get("aud")) != [self._client_id]: + raise PrincipalRefused("wrong-audience") + return _principal_of(claims) + + +def _principal_of(claims: dict[str, Any]) -> Principal: + kind, via = claims.get("kind"), claims.get("via") + if kind not in _KINDS or via not in _VIAS: + raise PrincipalRefused("unknown-major") + act = claims.get("act") + two_factor_at = claims.get("two_factor_at") + return Principal( + subject=str(claims.get("sub") or ""), + kind=cast(Kind, kind), + roles=tuple(str(r) for r in claims.get("roles") or ()), + via=cast(Via, via), + two_factor=claims.get("two_factor") is True, + ver=str(claims["ver"]), + email=claims.get("email"), + name=claims.get("name"), + actor=Actor(id=str(act.get("sub")), name=str(act.get("name"))) if isinstance(act, dict) else None, + two_factor_at=datetime.fromtimestamp(two_factor_at, UTC) if two_factor_at else None, + sid=claims.get("sid"), + ) + + +def _major(claims: dict[str, Any]) -> str | None: + ver = claims.get("ver") + return ver.split(".")[0] if isinstance(ver, str) else None + + +def _audience(aud: Any) -> list[str] | None: + if isinstance(aud, str): + return [aud] + if isinstance(aud, list): + return [str(a) for a in aud] + return None + + +def _unbase64(text: str) -> bytes: + return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4)) + + +def _integer(text: str) -> int: + return int.from_bytes(_unbase64(text), "big") diff --git a/src/jorisjonkers_commons/py.typed b/src/jorisjonkers_commons/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/contract.py b/tests/contract.py new file mode 100644 index 0000000..b83e0a0 --- /dev/null +++ b/tests/contract.py @@ -0,0 +1,55 @@ +"""The pinned contract, vendored by scripts/pin-contract.sh, and identity as a Fetch serving it.""" + +import base64 +import json +import urllib.parse +from datetime import datetime +from pathlib import Path +from typing import Any + +from jorisjonkers_commons.identity import Actor, Fetch, Principal, Request, Response + +CONTRACT = Path(__file__).resolve().parent.parent / "contract" + + +def contract_json(path: str) -> Any: + return json.loads((CONTRACT / path).read_text()) + + +def vectors(channel: str) -> list[tuple[str, dict[str, Any]]]: + """Every vector of a channel, by name.""" + return [(f.stem, json.loads(f.read_text())) for f in sorted((CONTRACT / "vectors" / channel).glob("*.json"))] + + +def principal_from(stated: dict[str, Any]) -> Principal: + """The Principal a vector states, as the contract's JSON names it.""" + actor = stated.get("actor") + two_factor_at = stated.get("twoFactorAt") + return Principal( + subject=stated["subject"], + kind=stated["kind"], + roles=tuple(stated["roles"]), + via=stated["via"], + two_factor=stated["twoFactor"], + ver=stated["ver"], + email=stated.get("email"), + name=stated.get("name"), + actor=Actor(id=actor["id"], name=actor["name"]) if actor else None, + two_factor_at=datetime.fromisoformat(two_factor_at) if two_factor_at else None, + sid=stated.get("sid"), + ) + + +def identity(client: str = "", secret: str = "", token: str = "", answer: Any = None) -> Fetch: + """identity as a Fetch: its published keys, and an introspection answer for one Client and token.""" + credentials = "Basic " + base64.b64encode(f"{client}:{secret}".encode()).decode() + + def fetch(request: Request) -> Response: + if request.url.endswith("/oidc/keys"): + return Response(200, (CONTRACT / "vectors" / "jwks.json").read_bytes()) + asked = urllib.parse.parse_qs((request.body or b"").decode()).get("token", [""])[0] + if request.headers.get("Authorization") != credentials or asked != token: + return Response(401, b"") + return Response(200, json.dumps(answer).encode()) + + return fetch diff --git a/tests/test_asgi.py b/tests/test_asgi.py new file mode 100644 index 0000000..ce40978 --- /dev/null +++ b/tests/test_asgi.py @@ -0,0 +1,197 @@ +import asyncio +from dataclasses import dataclass, replace +from datetime import UTC, datetime, timedelta +from typing import Any + +import pytest + +from jorisjonkers_commons.identity import ( + Actor, + Headers, + IdentityUnavailable, + Principal, + PrincipalReader, + PrincipalRefused, + principal_middleware, + principal_of, + refuse_tools, + require_machine, + require_recent_two_factor, + require_role, + require_user, +) +from jorisjonkers_commons.identity.asgi import Message, Policy, Scope + +NOW = datetime(2026, 10, 20, 12, 0, 30, tzinfo=UTC) +ADA = Principal(subject="ada", kind="user", roles=("user",), via="session", two_factor=False, ver="1.1", sid="s") +MACHINE = replace(ADA, kind="machine", via="machine", sid=None) + + +@dataclass +class Outcome: + status: int | None = None + location: str | None = None + closed: int | None = None + passed: Scope | None = None + + +def run(wrap: Policy, kind: str = "http", principal: Principal | None = None, **scope: Any) -> Outcome: + """What wrap, wrapped around a handler, did with a request: what it answered, or the scope it + passed on to the handler.""" + outcome = Outcome() + + async def handler(passed: Scope, receive: Any, send: Any) -> None: + outcome.passed = passed + + async def receive() -> Message: + return {"type": "http.request"} + + async def send(message: Message) -> None: + if message["type"] == "http.response.start": + outcome.status = message["status"] + outcome.location = dict(message["headers"]).get(b"location", b"").decode() or None + if message["type"] == "websocket.close": + outcome.closed = message["code"] + + state = {"principal": principal} if principal else {} + request = {"type": kind, "path": "/", "headers": [], "state": state, **scope} + + async def serve() -> None: + await wrap(handler)(request, receive, send) + + asyncio.run(serve()) + return outcome + + +class Answering(PrincipalReader): + """A reader answering one outcome, recording the Authorization header it was asked for.""" + + def __init__(self, outcome: Principal | Exception) -> None: + self.outcome = outcome + self.asked: str | None = None + + def read(self, header: Headers) -> Principal: + self.asked = header("Authorization") + if isinstance(self.outcome, Exception): + raise self.outcome + return self.outcome + + +def reading(reader: PrincipalReader) -> Policy: + return lambda app: principal_middleware(app, reader) + + +def test_the_middleware_passes_the_principal_on_to_the_handler() -> None: + passed = run(reading(Answering(ADA))).passed + + assert passed is not None and principal_of(passed) == ADA + + +def test_the_middleware_answers_401_to_a_refused_request() -> None: + assert run(reading(Answering(PrincipalRefused("missing")))).status == 401 + + +def test_the_middleware_answers_503_when_identity_cannot_be_asked() -> None: + assert run(reading(Answering(IdentityUnavailable("down")))).status == 503 + + +def test_the_middleware_reads_the_requests_headers_by_name_whatever_their_case() -> None: + reader = Answering(ADA) + + run(reading(reader), headers=[(b"authorization", b"Bearer t")]) + + assert reader.asked == "Bearer t" + + +def test_the_middleware_closes_a_refused_websocket_before_it_is_accepted() -> None: + assert run(reading(Answering(PrincipalRefused("missing"))), "websocket").closed == 1008 + + +def test_the_middleware_leaves_lifespan_events_alone() -> None: + reader = Answering(PrincipalRefused("missing")) + + assert run(reading(reader), "lifespan").passed is not None + + +def test_require_role_lets_through_a_caller_holding_the_generated_role() -> None: + assert run(require_role("editor"), "http", replace(ADA, roles=("editor",))).passed is not None + + +def test_require_role_answers_403_to_a_caller_without_the_role() -> None: + assert run(require_role("editor"), "http", ADA).status == 403 + + +def test_refuse_tools_answers_403_to_a_tool_acting_for_a_user() -> None: + tool = replace(ADA, via="tool", actor=Actor(id="mcp_1", name="Claude Code"), sid=None) + + assert run(refuse_tools(), "http", tool).status == 403 + + +def test_refuse_tools_lets_the_user_themselves_through() -> None: + assert run(refuse_tools(), "http", ADA).passed is not None + + +def test_require_user_answers_403_to_a_machine() -> None: + assert run(require_user(), "http", MACHINE).status == 403 + + +def test_require_user_lets_a_person_through() -> None: + assert run(require_user(), "http", ADA).passed is not None + + +def test_require_machine_answers_403_to_a_person() -> None: + assert run(require_machine(), "http", ADA).status == 403 + + +def test_require_machine_lets_a_machine_through() -> None: + assert run(require_machine(), "http", MACHINE).passed is not None + + +@pytest.mark.parametrize( + "policy", + [require_role("user"), refuse_tools(), require_user(), require_machine(), require_recent_two_factor(5)], + ids=["require_role", "refuse_tools", "require_user", "require_machine", "require_recent_two_factor"], +) +def test_a_policy_answers_401_without_a_principal(policy: Policy) -> None: + assert run(policy, "http").status == 401 + + +@pytest.mark.parametrize( + "policy", + [require_role("editor"), require_recent_two_factor(5)], + ids=["require_role", "require_recent_two_factor"], +) +def test_a_policy_leaves_lifespan_events_alone(policy: Policy) -> None: + assert run(policy, "lifespan").passed is not None + + +def test_a_policy_closes_a_websocket_it_refuses() -> None: + assert run(require_role("editor"), "websocket", ADA).closed == 1008 + + +def test_require_recent_two_factor_lets_a_fresh_enough_proof_through() -> None: + proved = replace(ADA, two_factor=True, two_factor_at=NOW - timedelta(minutes=4)) + + assert run(require_recent_two_factor(5, lambda: NOW), "http", proved).passed is not None + + +def test_require_recent_two_factor_sends_a_stale_session_to_step_up_and_back_to_the_same_page() -> None: + stale = replace(ADA, two_factor=True, two_factor_at=NOW - timedelta(minutes=6)) + + outcome = run(require_recent_two_factor(5, lambda: NOW), "http", stale, path="/pay", query_string=b"amount=3") + + assert (outcome.status, outcome.location) == (303, "/.auth/step-up?minutes=5&rd=%2Fpay%3Famount%3D3") + + +def test_require_recent_two_factor_sends_a_session_with_no_proof_to_step_up() -> None: + assert run(require_recent_two_factor(5), "http", ADA).location == "/.auth/step-up?minutes=5&rd=%2F" + + +def test_require_recent_two_factor_answers_403_to_a_personal_token_it_cannot_step_up() -> None: + assert run(require_recent_two_factor(5), "http", replace(ADA, via="personal-token", sid=None)).status == 403 + + +def test_require_recent_two_factor_reads_the_real_time_when_given_no_clock() -> None: + proved = replace(ADA, two_factor=True, two_factor_at=datetime.now(UTC)) + + assert run(require_recent_two_factor(5), "http", proved).passed is not None diff --git a/tests/test_conformance.py b/tests/test_conformance.py new file mode 100644 index 0000000..1fe018b --- /dev/null +++ b/tests/test_conformance.py @@ -0,0 +1,58 @@ +from datetime import datetime +from typing import Any + +import pytest + +from contract import identity, principal_from, vectors +from jorisjonkers_commons.identity import HEADER, Headers, Principal, PrincipalReader, PrincipalRefused + + +def outcome(reader: PrincipalReader, header: Headers) -> Principal | str: + try: + return reader.read(header) + except PrincipalRefused as refused: + return refused.reason + + +def expected(vector: dict[str, Any]) -> Principal | str: + refusal = vector.get("refusal") + return refusal if isinstance(refusal, str) else principal_from(vector["principal"]) + + +def reader_for(vector: dict[str, Any]) -> PrincipalReader: + now = datetime.fromisoformat(vector["now"]) + return PrincipalReader(vector["issuer"], vector["client"], fetch=identity(), now=lambda: now) + + +def bearer(token: str) -> Headers: + return lambda name: f"Bearer {token}" if name == "Authorization" else None + + +@pytest.mark.parametrize(("name", "vector"), vectors("signed-header"), ids=lambda v: v if isinstance(v, str) else "") +def test_every_pinned_signed_header_vector_reads_as_the_contract_says(name: str, vector: dict[str, Any]) -> None: + assert outcome(reader_for(vector), vector["headers"].get) == expected(vector) + + +@pytest.mark.parametrize(("name", "vector"), vectors("access-token"), ids=lambda v: v if isinstance(v, str) else "") +def test_every_pinned_access_token_vector_reads_as_the_contract_says(name: str, vector: dict[str, Any]) -> None: + assert outcome(reader_for(vector), bearer(vector["token"])) == expected(vector) + + +@pytest.mark.parametrize(("name", "vector"), vectors("introspection"), ids=lambda v: v if isinstance(v, str) else "") +def test_every_pinned_introspection_vector_reads_as_the_contract_says(name: str, vector: dict[str, Any]) -> None: + fetch = identity(vector["client"], "s3cret", "opaque", vector["answer"]) + reader = PrincipalReader("https://auth.example.test", vector["client"], client_secret="s3cret", fetch=fetch) + + assert outcome(reader, bearer("opaque")) == expected(vector) + + +def test_an_access_token_presented_as_the_signed_header_is_refused() -> None: + vector = dict(vectors("access-token"))["session"] + + assert outcome(reader_for(vector), {HEADER: vector["token"]}.get) == "wrong-typ" + + +def test_a_signed_header_presented_as_a_bearer_token_is_refused() -> None: + vector = dict(vectors("signed-header"))["session"] + + assert outcome(reader_for(vector), bearer(vector["headers"][HEADER])) == "wrong-typ" diff --git a/tests/test_fetch.py b/tests/test_fetch.py new file mode 100644 index 0000000..4d63caf --- /dev/null +++ b/tests/test_fetch.py @@ -0,0 +1,54 @@ +import socket +import threading +from collections.abc import Iterator +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +import pytest + +from jorisjonkers_commons.identity import IdentityUnavailable, Request, Response, urllib_fetch + + +class Identity(BaseHTTPRequestHandler): + """Answers a POST with its body and 200, and anything else with 404.""" + + def do_POST(self) -> None: + body = self.rfile.read(int(self.headers["Content-Length"])) + self.send_response(200) + self.end_headers() + self.wfile.write(self.headers["Authorization"].encode() + b" " + body) + + def do_GET(self) -> None: + self.send_response(404) + self.end_headers() + self.wfile.write(b"no such keys") + + def log_message(self, format: str, *args: object) -> None: + return None + + +@pytest.fixture +def identity() -> Iterator[str]: + server = ThreadingHTTPServer(("127.0.0.1", 0), Identity) + threading.Thread(target=server.serve_forever, daemon=True).start() + yield f"http://127.0.0.1:{server.server_address[1]}" + server.shutdown() + server.server_close() + + +def test_urllib_fetch_sends_the_request_and_returns_identitys_answer(identity: str) -> None: + request = Request("POST", f"{identity}/oidc/introspect", {"Authorization": "Basic d2ViOnM="}, b"token=t") + + assert urllib_fetch(request) == Response(200, b"Basic d2ViOnM= token=t") + + +def test_urllib_fetch_returns_an_error_status_as_an_answer(identity: str) -> None: + assert urllib_fetch(Request("GET", f"{identity}/oidc/keys")) == Response(404, b"no such keys") + + +def test_urllib_fetch_raises_identity_unavailable_when_nothing_answers() -> None: + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + closed = s.getsockname()[1] + + with pytest.raises(IdentityUnavailable): + urllib_fetch(Request("GET", f"http://127.0.0.1:{closed}/oidc/keys")) diff --git a/tests/test_principal_type.py b/tests/test_principal_type.py new file mode 100644 index 0000000..dce123f --- /dev/null +++ b/tests/test_principal_type.py @@ -0,0 +1,24 @@ +import os +import sys +from pathlib import Path + +import pytest + +from contract import contract_json + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) +from generate_principal import GENERATED, generate + + +def test_the_principal_type_is_what_the_pinned_schema_generates() -> None: + # PRINCIPAL_UPDATE=true task test + want = generate(contract_json("principal.schema.json")) + if os.environ.get("PRINCIPAL_UPDATE") == "true": + GENERATED.write_text(want) + + assert GENERATED.read_text() == want + + +def test_the_principal_type_is_not_generated_for_a_shape_the_generator_does_not_know() -> None: + with pytest.raises(ValueError, match="no Python type for n"): + generate({"title": "T", "type": "object", "properties": {"n": {"type": "number"}}}) diff --git a/tests/test_reader.py b/tests/test_reader.py new file mode 100644 index 0000000..bb81b52 --- /dev/null +++ b/tests/test_reader.py @@ -0,0 +1,310 @@ +import base64 +import json +from collections.abc import Callable +from dataclasses import dataclass, field +from datetime import UTC, datetime, timedelta +from typing import Any + +import pytest +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 +from cryptography.hazmat.primitives.hashes import SHA256 + +from jorisjonkers_commons.identity import ( + Headers, + IdentityUnavailable, + PrincipalReader, + PrincipalRefused, + Request, + Response, +) + +SIGNED_AT = datetime(2026, 10, 20, 12, tzinfo=UTC) +KEY = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + +def b64(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode() + + +def integer(n: int) -> str: + return b64(n.to_bytes((n.bit_length() + 7) // 8, "big")) + + +JWKS = { + "keys": [ + { + "kty": "RSA", + "kid": "k", + "alg": "RS256", + "n": integer(KEY.public_key().public_numbers().n), + "e": integer(KEY.public_key().public_numbers().e), + } + ] +} + + +def claims(**edit: Any) -> dict[str, Any]: + c: dict[str, Any] = { + "iss": "https://auth.example.test", + "sub": "ada", + "aud": "web", + "client_id": "web", + "exp": int(SIGNED_AT.timestamp()) + 60, + "ver": "1.1", + "kind": "user", + "via": "personal-token", + "roles": ["user"], + "two_factor": False, + } + c.update(edit) + return {k: v for k, v in c.items() if v is not None} + + +def sign(payload: dict[str, Any] | str, typ: str = "JWT", kid: str = "k", alg: str = "RS256") -> str: + head = b64(json.dumps({"alg": alg, "typ": typ, "kid": kid}).encode()) + body = b64((payload if isinstance(payload, str) else json.dumps(payload)).encode()) + signature = KEY.sign(f"{head}.{body}".encode(), PKCS1v15(), SHA256()) + return f"{head}.{body}.{b64(signature)}" + + +@dataclass +class Identity: + """identity as a Fetch, counting each question and answering what a test sets.""" + + keys: Response = field(default_factory=lambda: Response(200, json.dumps(JWKS).encode())) + introspection: Response = field(default_factory=lambda: Response(200, b'{"active":false}')) + fails: bool = False + keys_fetched: int = 0 + asked: int = 0 + urls: list[str] = field(default_factory=list) + + def fetch(self, request: Request) -> Response: + self.urls.append(request.url) + if self.fails: + raise IdentityUnavailable("down") + if request.url.endswith("/oidc/keys"): + self.keys_fetched += 1 + return self.keys + self.asked += 1 + return self.introspection + + +@dataclass +class Clock: + now: datetime = SIGNED_AT + timedelta(seconds=30) + + +def reader(identity: Identity, clock: Clock | None = None, secret: str | None = "s3cret") -> PrincipalReader: + at = clock or Clock() + return PrincipalReader( + "https://auth.example.test", "web", client_secret=secret, fetch=identity.fetch, now=lambda: at.now + ) + + +def bearer(token: str) -> Headers: + return lambda name: f"Bearer {token}" if name == "Authorization" else None + + +def reason(read: Callable[[], Any]) -> str: + try: + read() + except PrincipalRefused as refused: + return refused.reason + return "read" + + +def active(**edit: Any) -> Response: + return Response(200, json.dumps(claims(active=True, **edit)).encode()) + + +def test_the_issuers_keys_are_fetched_once_for_every_token_they_sign() -> None: + identity = Identity() + r = reader(identity) + + for _ in range(3): + r.read(bearer(sign(claims()))) + + assert identity.keys_fetched == 1 + + +def test_the_issuers_keys_are_fetched_again_for_an_unknown_key_at_most_once_a_minute() -> None: + identity = Identity() + clock = Clock() + r = reader(identity, clock) + r.read(bearer(sign(claims()))) + reason(lambda: r.read(bearer(sign(claims(), kid="rotated")))) + reason(lambda: r.read(bearer(sign(claims(), kid="rotated")))) + clock.now += timedelta(minutes=1) + reason(lambda: r.read(bearer(sign(claims(exp=int(clock.now.timestamp()) + 60), kid="rotated")))) + + assert identity.keys_fetched == 2 + + +def test_a_key_id_the_issuer_does_not_publish_is_refused() -> None: + assert reason(lambda: reader(Identity()).read(bearer(sign(claims(), kid="forged")))) == "unknown-key" + + +def test_a_key_id_that_is_no_string_is_refused_without_fetching_the_keys() -> None: + identity = Identity() + head = b64(json.dumps({"alg": "RS256", "typ": "JWT", "kid": ["k"]}).encode()) + + assert reason(lambda: reader(identity).read(bearer(f"{head}.e30.c2ln"))) == "unknown-key" + assert identity.keys_fetched == 0 + + +@pytest.mark.parametrize( + "keys", + [Response(500, b""), Response(200, b"not json"), Response(200, b'{"keys":[{"kty":"RSA","kid":"k"}]}')], + ids=["an error", "no JSON", "a key without its numbers"], +) +def test_issuer_keys_that_answer_badly_are_no_refusal(keys: Response) -> None: + with pytest.raises(IdentityUnavailable): + reader(Identity(keys=keys)).read(bearer(sign(claims()))) + + +def test_issuer_keys_that_cannot_be_reached_are_no_refusal() -> None: + with pytest.raises(IdentityUnavailable): + reader(Identity(fails=True)).read(bearer(sign(claims()))) + + +def test_issuer_keys_may_be_published_without_a_keys_list() -> None: + identity = Identity(keys=Response(200, b"{}")) + + assert reason(lambda: reader(identity).read(bearer(sign(claims())))) == "unknown-key" + + +def test_issuer_keys_that_are_not_rsa_are_passed_over() -> None: + identity = Identity(keys=Response(200, b'{"keys":[{"kty":"EC","kid":"k"}]}')) + + assert reason(lambda: reader(identity).read(bearer(sign(claims())))) == "unknown-key" + + +def test_an_access_token_that_is_no_jws_is_refused() -> None: + assert reason(lambda: reader(Identity()).read(bearer("a.b.c"))) == "bad-signature" + + +def test_an_access_token_not_signed_with_rs256_is_refused() -> None: + assert reason(lambda: reader(Identity()).read(bearer(sign(claims(), alg="HS256")))) == "bad-signature" + + +def test_an_access_token_whose_signed_payload_is_no_json_object_is_refused() -> None: + assert reason(lambda: reader(Identity()).read(bearer(sign("[1]")))) == "bad-signature" + + +def test_an_access_token_whose_audience_lists_only_the_apps_client_is_read() -> None: + assert reader(Identity()).read(bearer(sign(claims(aud=["web"])))).subject == "ada" + + +def test_an_access_token_whose_audience_is_neither_a_string_nor_a_list_is_refused() -> None: + assert reason(lambda: reader(Identity()).read(bearer(sign(claims(aud=7))))) == "wrong-audience" + + +def test_an_access_token_stating_a_kind_v1_does_not_know_is_refused() -> None: + assert reason(lambda: reader(Identity()).read(bearer(sign(claims(kind="robot"))))) == "unknown-major" + + +def test_an_access_token_stating_no_roles_or_subject_holds_empty_ones() -> None: + p = reader(Identity()).read(bearer(sign(claims(roles=None, sub=None)))) + + assert (p.roles, p.subject) == ((), "") + + +def test_an_access_token_stating_no_ver_is_refused() -> None: + assert reason(lambda: reader(Identity()).read(bearer(sign(claims(ver=None))))) == "unknown-major" + + +def test_an_access_token_stating_no_expiry_is_refused() -> None: + assert reason(lambda: reader(Identity()).read(bearer(sign(claims(exp=None))))) == "expired" + + +def test_an_opaque_token_that_is_revoked_stops_working_within_thirty_seconds() -> None: + identity = Identity(introspection=active()) + clock = Clock(SIGNED_AT) + r = reader(identity, clock) + r.read(bearer("idapp_tool")) + identity.introspection = Response(200, b'{"active":false}') + clock.now += timedelta(seconds=29) + r.read(bearer("idapp_tool")) + clock.now += timedelta(seconds=1) + + assert reason(lambda: r.read(bearer("idapp_tool"))) == "inactive" + + +def test_an_opaque_token_is_asked_about_once_per_thirty_seconds() -> None: + identity = Identity(introspection=active()) + r = reader(identity) + + for _ in range(5): + r.read(bearer("idapp_tool")) + + assert identity.asked == 1 + + +def test_an_opaque_token_refused_once_is_refused_again_without_asking() -> None: + identity = Identity() + r = reader(identity) + reason(lambda: r.read(bearer("idpat_gone"))) + + assert (reason(lambda: r.read(bearer("idpat_gone"))), identity.asked) == ("inactive", 1) + + +def test_an_opaque_token_is_refused_without_asking_by_an_app_without_a_client_secret() -> None: + identity = Identity() + + assert (reason(lambda: reader(identity, secret=None).read(bearer("idpat_script"))), identity.asked) == ( + "inactive", + 0, + ) + + +def test_an_opaque_token_that_cannot_be_asked_about_is_no_refusal_and_is_asked_about_again() -> None: + identity = Identity(introspection=Response(502, b"")) + r = reader(identity) + with pytest.raises(IdentityUnavailable): + r.read(bearer("idapp_tool")) + identity.introspection = active() + + assert r.read(bearer("idapp_tool")).subject == "ada" + + +@pytest.mark.parametrize("body", [b"not json", b"[]"], ids=["no JSON", "no JSON object"]) +def test_an_introspection_answer_that_is_no_json_object_is_no_refusal(body: bytes) -> None: + with pytest.raises(IdentityUnavailable): + reader(Identity(introspection=Response(200, body))).read(bearer("idapp_tool")) + + +def test_the_answers_kept_are_dropped_once_ten_thousand_are_kept_so_the_first_is_asked_about_again() -> None: + identity = Identity(introspection=active()) + r = reader(identity) + for i in range(10_001): + r.read(bearer(f"idapp_{i}")) + asked = identity.asked + + r.read(bearer("idapp_0")) + + assert identity.asked == asked + 1 + + +@pytest.mark.parametrize( + "authorization", [None, "Basic dXNlcjpwYXNz", "Bearer ", "Bearer"], ids=["none", "basic", "empty", "bare"] +) +def test_a_request_without_a_bearer_token_carries_no_principal(authorization: str | None) -> None: + assert ( + reason(lambda: reader(Identity()).read(lambda name: authorization if name == "Authorization" else None)) + == "missing" + ) + + +def test_a_reader_asks_the_issuers_own_endpoints_by_default() -> None: + identity = Identity() + r = PrincipalReader("https://auth.example.test/", "web", client_secret="s3cret", fetch=identity.fetch) + reason(lambda: r.read(bearer("idpat_script"))) + reason(lambda: r.read(bearer(sign(claims())))) + + assert identity.urls == ["https://auth.example.test/oidc/introspect", "https://auth.example.test/oidc/keys"] + + +def test_a_reader_names_its_issuer_and_its_client() -> None: + with pytest.raises(ValueError, match="names its Issuer and its Client"): + PrincipalReader("", "web") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..3507e99 --- /dev/null +++ b/uv.lock @@ -0,0 +1,768 @@ +version = 1 +revision = 5 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "ast-serialize" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/d5/5dc1ee8a80c3b20a0b63c61b43ba6bc53517faecd1def47bcd752d22268e/ast_serialize-0.13.1.tar.gz", hash = "sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025", size = 955137, upload-time = "2026-10-17T00:04:17.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/a8/fbd136b9f70c59e75f59a8e974c84102976f1201828b4f886480ba956936/ast_serialize-0.13.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3e129d44b0cffbea24a7f40117ce49e30993c3d119e584d4817ebd6f87246456", size = 897306, upload-time = "2026-10-17T00:02:52.479Z" }, + { url = "https://files.pythonhosted.org/packages/50/3f/2ad51e9823a790e7a70fd781e9595ab3abeace27f3aefcaa1939457efd72/ast_serialize-0.13.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:523d45bab16ddaf1399c04c4fe81bbf180f81e33c2f7bcbe76be56b0f7311ab4", size = 1236082, upload-time = "2026-10-17T00:02:54.82Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5c/8e959db5cc2237f9d4c560a8e2253c98f9746883fb4774651e4cda458901/ast_serialize-0.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:638333fb8ae72c62753f9e794fda20290883ddc65b0d85e26e08f8bb227567ad", size = 1221242, upload-time = "2026-10-17T00:02:56.395Z" }, + { url = "https://files.pythonhosted.org/packages/d6/05/ab1c56c3b7adcac88764bba609502d6b4989e449b2dd60eb478e1d20e369/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc34c670747d85d4232834665e247009413eaebe47c69eaf60a5c439aaecaed9", size = 1282105, upload-time = "2026-10-17T00:02:58.275Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f4/41981fcd2ddffe0760e5326be9dcbd2eda9478a8430a637e5123bd8fcb26/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c8ebd419811c435812d242fb2a173e57dfede198c9ac9a0a584666ebc9b9331", size = 1287905, upload-time = "2026-10-17T00:03:00.068Z" }, + { url = "https://files.pythonhosted.org/packages/6a/43/b783addc5681153707121991b7b47892f7e9c8ba58a0d03b71a3b023ba47/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8899c038b1c15ae8b98e379057fb3ef398ceb0b21d1737a8156dd5a0923b7e51", size = 1541806, upload-time = "2026-10-17T00:03:01.503Z" }, + { url = "https://files.pythonhosted.org/packages/47/10/969db095038ee15270f65ad20d1129c5e4da605bf7e93b70ae640f1c8a9f/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e14c4de98f965647a74e6eaf37e20207124e45451ccd36e84a6f6ebab45e0372", size = 1309946, upload-time = "2026-10-17T00:03:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/06/dd/7c623d2fb0b868948952d1787e629a43b866f7d4caefd5445e20aefacec4/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3e8a3eff69af640db985360fef35073fb46956dfc3e5c614cf00920d2746dd", size = 1302773, upload-time = "2026-10-17T00:03:04.378Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c2/4d49469110a19a44fc6269f4fd59679bcf84df068f185f89b73bee4da19b/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:75d7750fe98f6f35a96c43aba8d98a2e623e22c778a0c17ab9ede92eb10f5d82", size = 1299579, upload-time = "2026-10-17T00:03:05.88Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/2d381f1f262dda15affc8ad643fba1619eb07e7d9f8bd904deffef183006/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e8ddd63e80fc280c2f790157386f27f9b9f6ccb9c4f8e803b863cbfb085769", size = 1355969, upload-time = "2026-10-17T00:03:07.321Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/2266794e6c25fa9ce51ce04a9bf4b7e330e3ac9351ba3fa14e487678acdb/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9221e09496fb942901993c1812ee87950cc0ecbb514c7c0f1f5fa6d78f701ed8", size = 1460227, upload-time = "2026-10-17T00:03:08.733Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d6/14deb28e5d8f328f626a0778ea2cf98fe908f2bbcccd804cb6f449ee1782/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7a5c38bdb93669c596f94fb7204dd6ebe4fadb19058ac9a2faf2948c2a175f96", size = 1562691, upload-time = "2026-10-17T00:03:10.25Z" }, + { url = "https://files.pythonhosted.org/packages/f5/19/7a28c0e5ba54f0d3e054f847a2a549b748cb179ab433321a8dc1b39e398d/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:acf4e9f055d75058ab01536da330648746f2f82b7697a85230547f2d5ebe895c", size = 1558669, upload-time = "2026-10-17T00:03:11.799Z" }, + { url = "https://files.pythonhosted.org/packages/18/9a/f9e2690eab58217f1d395a964a78efa7eaf813a1e38be8dfd9a8d45d0840/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87427913c325adc811e81e7c9a757b061f47ef1e29114ac479aa0953a2687a98", size = 1665071, upload-time = "2026-10-17T00:03:13.284Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/ef5b159a0d7161b404f05e7ec21cf7735ee7edc10c000cd2cbbc7bb6b69a/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ad88921f95b92bc144d9ad059a0e16f8c1ae69f5239f2d3c6536fd2774f73dfe", size = 1473364, upload-time = "2026-10-17T00:03:14.563Z" }, + { url = "https://files.pythonhosted.org/packages/1d/87/6e8ac4a865124a12cc4f915bf1bcfac3bba426cd5f58d3712a873ee61012/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f8b17e2921e56621bcb46792bd3e0b82ca3b85de78d5936e7b8174ba1758b0aa", size = 1500566, upload-time = "2026-10-17T00:03:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1e/29155f1621433008013ad5c940a03d41a676be0680c41527040472d37583/ast_serialize-0.13.1-cp314-cp314t-win32.whl", hash = "sha256:2f9a9ce8b0994f757b941f7f7fe384a6666b4162b941ebc5c248081baea18e47", size = 1128674, upload-time = "2026-10-17T00:03:17.481Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/2506da65143ea138de5822e9173e0b72320a2f70e23bd723f044033175f1/ast_serialize-0.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5d99910e6b9a9b9360bcca2ae99aee36cc912ce058df2a57d1c33099b6cb2593", size = 1163306, upload-time = "2026-10-17T00:03:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8f/c7cb22638899b0cf15908809c270560e46395e65604bc59e4b623ab9f405/ast_serialize-0.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a789cbe14d0a0e9ec9b32a456e5abc81ad8799479c3cfd88a29e3442262aebe6", size = 1133775, upload-time = "2026-10-17T00:03:20.377Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e2/b0c140fe50fa39dd249e382f7733af6f816c90021fe7959a0b026f86237f/ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:23d768369b89d551077dc173da6892fad00443338417523b6732f4e8ba4a7ed5", size = 1236160, upload-time = "2026-10-17T00:03:21.945Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/a8a2ca21e3943555b48221f8fba546877a7ad39efaf6505506524967d7c4/ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:68f8829d114dab6c9db4a8bbb8d0e342958faee1b5e15a29022678df639960f8", size = 1222214, upload-time = "2026-10-17T00:03:23.462Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/2f58f2ba64e4799b39de85c140aef09a03aa6ea53be75dd292a9d9b9d8ae/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e385b17d5bde892a6184003a9aff3518387908b73f8f5ea539313c07c8c0ede", size = 1282979, upload-time = "2026-10-17T00:03:24.695Z" }, + { url = "https://files.pythonhosted.org/packages/b1/af/ca6087fe84210c1ccd43e3182917bb70649fa1e69745a90c1d59ba08fea8/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02686b281ec9b8e9614f6f0db27e12e4cac515f336c5d05b678bc9eb73ff2af5", size = 1288596, upload-time = "2026-10-17T00:03:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2c/4ef964c763329a0347125e745ec0451ba4fbaceec9efe2ac729b7cd55f17/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8ab2c2585b2e22e48cd4aff2136fce4eab4ced569b85f150ef9a5a5d6f34d49", size = 1545540, upload-time = "2026-10-17T00:03:27.52Z" }, + { url = "https://files.pythonhosted.org/packages/bb/85/aa961dbb75813cfa94de0ce9a16a9510c226b95f604c405421e727f44bb0/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c652d9f5d52d8b81eff3ff2c95c9dd0f281de45f6ad5d3fa95527550c396454a", size = 1309374, upload-time = "2026-10-17T00:03:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/f2b67c327d9f9533e41791804403ccc2dc457821195a2e51f0b587a90b90/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121e1d08d58b75c15491cc3003609cbad71ac2b0016043c36d30a6b2032b6605", size = 1303466, upload-time = "2026-10-17T00:03:30.451Z" }, + { url = "https://files.pythonhosted.org/packages/73/1d/33f18b81d1bb1f5d682943ea92fda8130a6593627f12f6201668d40824fd/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:30cf89b6a6844542d116c758725d13de5b97b9410a51ee577715f761270d0c1c", size = 1300576, upload-time = "2026-10-17T00:03:32.102Z" }, + { url = "https://files.pythonhosted.org/packages/25/cb/64914144925f1a4abdd9217e717b122255c38fc5ebe4342f92052f3bacbc/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:732aaae9e498c973fc63fd7f308009b57c4afa6ff59d0b674d894e57d0bca01a", size = 1355960, upload-time = "2026-10-17T00:03:33.676Z" }, + { url = "https://files.pythonhosted.org/packages/ee/80/bd29379fab2bce19adc0bf9177002b11b23dfe8d8fecf409998b458b4624/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d9c1a920b2aa0c1e016bed7721689717034ca0dba64ac3a3ff243b003d4e8dd8", size = 1459779, upload-time = "2026-10-17T00:03:35.09Z" }, + { url = "https://files.pythonhosted.org/packages/83/89/012dbe733de05ffdf8e40ca0f9fa5d417cb16ec93a74463c57c001914198/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:847cad4c22953823f5e76d0b126c6f9da670cfa45b1df41b9324e22189d77611", size = 1563399, upload-time = "2026-10-17T00:03:37.285Z" }, + { url = "https://files.pythonhosted.org/packages/69/37/ee4bcb0358bee38744eb0f18560446572d095fe3bf6b63d6f739d08a839c/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:aca07de3ca8e3d9f2b716c3c31f4f384fe4bd2d03e09912417cff7a3e08036f7", size = 1558808, upload-time = "2026-10-17T00:03:38.711Z" }, + { url = "https://files.pythonhosted.org/packages/54/1c/ff44c5355251d61ef22934c4430bb31a61de4781d25a992154aac9b8c82a/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:5db12d843d931b9d1f1519ce66b9cbfe72d8563b75c1204d255322254d1964dd", size = 1666324, upload-time = "2026-10-17T00:03:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bb/6da4bfc8aa3403e6447607131bf3463acaf954d990c6c8b6787c1902ddfe/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:fb8021f4dc3296de18737e3b8b34994411da725374d74a0b9eb32a738f2a23d0", size = 1474089, upload-time = "2026-10-17T00:03:41.715Z" }, + { url = "https://files.pythonhosted.org/packages/21/97/897e33bef2eefd880e6350e375cc66bc5bcd00a139be480fd655f5b3f095/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:0d764913374ba21df8cd618045b371ed104ce55c6276de7c59e554a4f5ba26fd", size = 1501542, upload-time = "2026-10-17T00:03:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/821211ac83870c27e4412381b7c7838336456b40575bf840e4680b2018d9/ast_serialize-0.13.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:2bad302ae2427454ef9dab71bd5c3ad67188860c39d36daee38f72031edb39c4", size = 1126118, upload-time = "2026-10-17T00:03:44.616Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/2b2d5e6c4fa352874a80618e742ffd32d249dee6c119f4233b51afc6b8f4/ast_serialize-0.13.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:032b09d86c785c3984e975378b7861f2a68ac44e22ba7b07b93a536b2524eeb8", size = 1163766, upload-time = "2026-10-17T00:03:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/d610cfcdabaa44fd62a54ca1fc57a7dba120643798b089a14914973562de/ast_serialize-0.13.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:929eda0c419e622ab49df3a4c45f81c57dcc10874bbad55d0470a4a03324dac3", size = 1134043, upload-time = "2026-10-17T00:03:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/0e30f9eddf7d44ccf9d25979030754fd0457af61fa4279acb8c293a04f97/ast_serialize-0.13.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bf53a3ba1cbe45399fc1e4ccd684b8f2a11cca450f89706b79122f478534c6f5", size = 897546, upload-time = "2026-10-17T00:03:50.093Z" }, + { url = "https://files.pythonhosted.org/packages/81/92/d705be88ff452b9ba0088b554a0cbd308fd01879da092b33fd2ad7d1aa60/ast_serialize-0.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4bcf1d5f4eefecaa325a34ecf079b2741189e7d0821523f92cb343a389a67489", size = 1242416, upload-time = "2026-10-17T00:03:51.521Z" }, + { url = "https://files.pythonhosted.org/packages/60/c2/98370845a7f35cb8a25782e78284b48e01bf3128339881d48d613f304f8f/ast_serialize-0.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1b158f1c9e3a0d386913924c57995d6df78224868a4d0f215bdd41587c97b34a", size = 1229729, upload-time = "2026-10-17T00:03:52.825Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8b/2d7ffed76addd9d3fcf37fb171400942735c8912264c763561a85407f6a8/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a6201b69ce42e919f2fd9a559138d5410b5d8afff7c3a6b1f500b0481ff7d8", size = 1292255, upload-time = "2026-10-17T00:03:54.148Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2a/bb9e0e719d89cc950fadfcfcc9cb4bc7640419c4228c852f031ab8abf440/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eb0be64c9b8f1ed26637eef345abe75ea90351c92c85a8aec7838b3af971011", size = 1298501, upload-time = "2026-10-17T00:03:55.559Z" }, + { url = "https://files.pythonhosted.org/packages/7a/32/13ce07dce48226f5bd9cfd8f708ec101a8be05aa430cda4918ce31ce8c82/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2427bddfca208e84de700044fadf9aef2769a70bae740a1bdabb89ee9e1117", size = 1553758, upload-time = "2026-10-17T00:03:56.952Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fb/9b51d186db0c20ca28cbaaefd50f03b573c89327b9a2627820cbce22196b/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88df960e67184a71874e5b2ee014f87d1254a0dbc1f903c7bd7b1ab7733ecaef", size = 1318258, upload-time = "2026-10-17T00:03:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fa/da311559dd3dae9af5825cf21660775a929b970f72e3786af3f2cd667944/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f99786e0d62082a5b121b3437f3167562133711df22128de1e9886f57f0b226", size = 1313260, upload-time = "2026-10-17T00:04:00.194Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/97a4b9e5b4776d72f47fcd9a09a497bbf68ec8c1ae074307a2c51e410d85/ast_serialize-0.13.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:a01b515b25a03184eff4acef25b981ae007916379682e908fc8785b342a0bf3e", size = 1309992, upload-time = "2026-10-17T00:04:01.575Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c4/62f85121a80cd451ef04f550cf0f7ea0ad9ff46cc4e9a5b9ae439b6527b1/ast_serialize-0.13.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29422d7bcd9e7f39411571c36cec45ce8a7a9b3ad4b2e7aa64497c15a9e397f6", size = 1362715, upload-time = "2026-10-17T00:04:02.881Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/c5f4a545f72a7681e3295bab6e3600ee0862ec4391bed961fe9f735b67e0/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ffb0f548a7dc126067fdfb7b2325ab5dfcdeecc29faf5e0f0461c96d6ebfa3d", size = 1469931, upload-time = "2026-10-17T00:04:04.181Z" }, + { url = "https://files.pythonhosted.org/packages/38/a9/65aed4f8fa7a001a7cfdc03e68e844517ab041ba6fb614b7d78a1d6ac056/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2876444a5f87a7c76f371af3c07c2fda563a08bc8af52568df812e8aba4bf568", size = 1570945, upload-time = "2026-10-17T00:04:05.778Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8c/44b2da991cede79bfbf52089f33a5f0168945466b3d9cd38b5dcf08e7130/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:885a62ec6481ce58ab24c6357360d7a82023b62dd7e5e7ceccf59e9662a3272b", size = 1569978, upload-time = "2026-10-17T00:04:07.347Z" }, + { url = "https://files.pythonhosted.org/packages/46/e4/109b2a5beca700237767d5add920c2f8e1ba9bd5799b0faf143d0e9cf4ef/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2015fd0165e463aac3b1aadc6a1fa1e5820c14ca950821f4fe02f8a3f6a388d6", size = 1676082, upload-time = "2026-10-17T00:04:08.7Z" }, + { url = "https://files.pythonhosted.org/packages/73/e0/7df70f6dbb11b5762da5416318f08316a1c51348e0d69ec79ca5f98f836a/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:eb41e1906502ac80d24ff343f45008552a3f5b5158607959fe6a07dbfc6eb037", size = 1483207, upload-time = "2026-10-17T00:04:10.162Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bc/523aa1c08bd69b5654621beb3dc1a9035715a4a63762fb8d219d6fb3eb81/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ca9acc67f796aa2554fa3037dccc022a4b08b38b2ad153edfac9c9bb7cb1f3c6", size = 1512140, upload-time = "2026-10-17T00:04:11.589Z" }, + { url = "https://files.pythonhosted.org/packages/66/83/6607060659998f00afdea0d8375c1e6e2b7d515ab905090d9a5a02f48851/ast_serialize-0.13.1-cp39-abi3-win32.whl", hash = "sha256:4f98da562e5901d69c7dd30885594c5438f859edf354e9ac7517d188fb026c3a", size = 1131889, upload-time = "2026-10-17T00:04:12.953Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6f/bafd6490fe7b355627fabe95f4417e3d02da93102c8552550eb9a0774e1e/ast_serialize-0.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:ddf7cbc9c8abf70102c4a088efe154bb86bfb825b0a10e556d2b15972cf99bd7", size = 1171153, upload-time = "2026-10-17T00:04:14.362Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/4235f4b375a67a37ac595bf478a5407a744eaf7b64113c1c3a5432b8d954/ast_serialize-0.13.1-cp39-abi3-win_arm64.whl", hash = "sha256:48f11d96c9ec71119612f8e79a8c55c12f2b136716d68347433c561d27a25f43", size = 1139035, upload-time = "2026-10-17T00:04:15.767Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/55/d1eaf3e73781174340a00dc1ba2aee8a65f82fadb18e2797b192b6b3925b/coverage-7.16.2.tar.gz", hash = "sha256:ca64d9f1f384f151b9511bec01126072acd2f313439f8ed015a22d8790aab6fa", size = 971999, upload-time = "2026-09-27T12:29:01.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/fa/ce3baf63d85b730398d92a7162f486f3a5e4e2cc3382a02488b3943725ba/coverage-7.16.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:732d950e51f3ba4fb6209c73250f3e8924fefca42953ee04a9e65d8c02414d7d", size = 223797, upload-time = "2026-09-27T12:25:54.756Z" }, + { url = "https://files.pythonhosted.org/packages/7a/57/9ba29c2aac7f756d479f03d45762120060f0f988788001001bf36e0e6fca/coverage-7.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dca0bb66b4c3d624ba047887bf70270030c150692d543cb501293dc38a9f4b5", size = 224303, upload-time = "2026-09-27T12:25:56.214Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/0d6d60906dca7d28cc1e3fce12a9861801c4fbb6cbf220ad78cd059c9467/coverage-7.16.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:af2a2a8c7c74de0559e0c368d94c8def9e16c58faaee33a0bf081057c4227e3b", size = 254715, upload-time = "2026-09-27T12:25:57.755Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b8/9198b865679379fb165c689c64f6e11105ef380f6bd1c7673e83f73d9f5c/coverage-7.16.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db5f8394e17f877a625b257f2ba0ce8e728a499c2c1579ad66220272cd3df510", size = 256627, upload-time = "2026-09-27T12:25:59.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/79/9521462cb6072fe394701bc8974b74afd576c9c9355156c7844e1a86a42b/coverage-7.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3146d2317c75f70df2509066d979dadd941f7021cdf9b5db4bcd8568258e25", size = 258724, upload-time = "2026-09-27T12:26:00.691Z" }, + { url = "https://files.pythonhosted.org/packages/a6/76/8d7d5d633db9fe0f3182fedc731bf09f9bcf2366055735152504ad614677/coverage-7.16.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e1d0ced76318bab499693ff25f64faa343415187cb2e4d7befdfdd391a1cf6a", size = 260692, upload-time = "2026-09-27T12:26:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a7/76cb09c89ba46d74d37428bf93251fc14fb0bbe9e05cc2a5ef61773d318a/coverage-7.16.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:af98ad5ed9d6daaca956201e00bb429a7eb2b080426686f70a20353e0f9839f5", size = 254800, upload-time = "2026-09-27T12:26:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/72/b6/2351c1979aaeb5b4a8091a75b90ca997ad60de36e181ddba267cf61dac97/coverage-7.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d56e4d21c56d2046447733f8b118409597db48c01efe898ee9ac24e858ec2d6", size = 256423, upload-time = "2026-09-27T12:26:04.751Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f4/ad9a4f8b5cb2d494fa9452b546fe742ed2f9d3847cc14c05e36279a3e649/coverage-7.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d5d0e3b660506fb84f995814e3118a21efdc0c8eb80127da1be627d90093c17", size = 254487, upload-time = "2026-09-27T12:26:06.082Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1f/a520470472f3e8b01169bf42162b1470c9ba992230432f62ca36269bf3a0/coverage-7.16.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:17228fbca0f22976f797be94e975dcd237799c657d49551c7de1e0654d1202e9", size = 258259, upload-time = "2026-09-27T12:26:07.513Z" }, + { url = "https://files.pythonhosted.org/packages/09/d2/ff26d5938274745855fa61cfcba0245c88ccc10d98d2cbd96064f16cd5a7/coverage-7.16.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:bc0b0ac781d489304b741269857f1f8338b7a26b1b89c06c0344658001ec0035", size = 254216, upload-time = "2026-09-27T12:26:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/5d832d3b06785d9f53267e4f2724a9f60c312eee6ebed9063a461d0d3b45/coverage-7.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bf1bd822ec4e387ed245bed0d71151582cf7be9e5309bc4145eefe36083d5878", size = 255125, upload-time = "2026-09-27T12:26:10.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/4d/1d33edbc2fcf7d99e384e393e712aa5a2ebbbd8409825357815982207976/coverage-7.16.2-cp311-cp311-win32.whl", hash = "sha256:7ed238d227e23cc300c3d464babdaf9f6ddc740aa1b15a77ae96136e6a7c4516", size = 225898, upload-time = "2026-09-27T12:26:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7c/676df4882118756c4f8f560c954eddb93e166d84dda8c5f0b6a829689bde/coverage-7.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:a90700f743e29aa3d75a6ff5f01953176a889c00e526194bc4d281731b88d99d", size = 226369, upload-time = "2026-09-27T12:26:13.375Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0e/a457f4a461b3c5610d845137fdd45fa465e011a64c25af440518ab1f4e41/coverage-7.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:a336eec40e3520d369b8a6cdabb4f596e69a8b42927ca074aa1452fed943238a", size = 226344, upload-time = "2026-09-27T12:26:15.127Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/f8296c63c5d542f3d21aed685e56b7031a419037d155bb3382fc0940d249/coverage-7.16.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:218d742afca2b5ad5ca759e93eddedfbcc6eadf8322f080dcefc40b7bd4e2d48", size = 223969, upload-time = "2026-09-27T12:26:16.753Z" }, + { url = "https://files.pythonhosted.org/packages/90/23/6f3dcb1423a0d43216e402ea1746e4a7c7c44f38896b97dd573790f56a40/coverage-7.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9a638be322a8d76a41cdb17781c7f82aaee6a66493d8ffb7e2c09ee22423d99", size = 224328, upload-time = "2026-09-27T12:26:18.15Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7d/8f3b6dc920e3fc6732f7678785a2091db439f186afbec30dbf2214d9b1f7/coverage-7.16.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:724bd0f1e81856b35e59fc98cf7b4e544a3cb662e4e0864dca73d4326ee9d808", size = 255832, upload-time = "2026-09-27T12:26:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/d1/36/6c45f15be4eca4ac1062c6a55a323286494c99726a7e58951fe85967ac08/coverage-7.16.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5375ebd99038021b35e99dc88255022912c06565d316212f4a576e4b08d30f5d", size = 258571, upload-time = "2026-09-27T12:26:21.199Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/b54cbeba3ad89082c2e441278681859e538322cc34b84b2af7ebff00080f/coverage-7.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a076277ca9f5750cc230f0f578ebd2620cec60255b25707361699fef6fb465c", size = 259680, upload-time = "2026-09-27T12:26:22.822Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a2/0dc65ec3d61930e1e4c2e371763b15eb4290896eb343a12d5d3091308116/coverage-7.16.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58d4a54c6ea672afef66d49be922a2c69826c5ae1a42a9cd94f0c9c2bacdf800", size = 261945, upload-time = "2026-09-27T12:26:24.336Z" }, + { url = "https://files.pythonhosted.org/packages/d6/93/5fad7a61f2c14e08e98946fc31c1c7ffc1195061bf3fdc351db3be77a863/coverage-7.16.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcbcfcc059117284c603ff8cb61a65872512882f84a8cf0339241f7f7c2f148", size = 256191, upload-time = "2026-09-27T12:26:25.89Z" }, + { url = "https://files.pythonhosted.org/packages/2d/47/74e5de9227b939ece9f64e729645ddc4296bea10dbfa98721c1333c8be2e/coverage-7.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afdf43b72ef3876c1fe66423b91466e37877c9e81e8cec70542b7e8525b9d1b7", size = 257610, upload-time = "2026-09-27T12:26:27.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/2cf28d40b43645d1b72388fe3ee7f7c747533a6a9557bb8c24a7ae74fe1a/coverage-7.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9acc7f7ec4a1b5f89bd929fde5b8a714f6fafdc6cc18725413d510aa082b47ad", size = 255757, upload-time = "2026-09-27T12:26:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3d/7c149fd99fc8bbc39c80db5e688d1d39fd040be2ecb78b8335a51a55b9c0/coverage-7.16.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:80d3f7b48d43ee8fc5e8707a8adb43d743a5a1a85256c25a24f9d6d0e2238fa6", size = 259822, upload-time = "2026-09-27T12:26:30.515Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/b283fce09d5995e227bd8e513358dd7471bedc0f78abc85a925ebdb0a2f6/coverage-7.16.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:126d1af8804d7224421fe991ff65d3ce649081560df7a98b1a5ffff07f9923bd", size = 255325, upload-time = "2026-09-27T12:26:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/bf/91/f3325edf0c4223fb1fe1532b8dbef2a1d2f729459a9a7d1a44d073bae534/coverage-7.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c19cd6d025c1673f22afcd22c7df8a662d779e05d8e3fa6820c22afb895b0206", size = 257191, upload-time = "2026-09-27T12:26:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/c4/89/21eb5e83ecf2eed523c4eb3d65ae513cd082c8fd1b6deb34c4cb6c332f97/coverage-7.16.2-cp312-cp312-win32.whl", hash = "sha256:152877cdc8a07264882cfcd503ba56a3ef6cba56a70e8c70f6eb8ffd7384789a", size = 226034, upload-time = "2026-09-27T12:26:35.021Z" }, + { url = "https://files.pythonhosted.org/packages/db/de/e3ad6d864c0833624b4f1f9b53f9e58e116c945e5e965c3f1e172c5e84cd/coverage-7.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:e6c52d3307824ff93b39efd99e4185d557db40bd841452abfb32e5d9151ca162", size = 226569, upload-time = "2026-09-27T12:26:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c1/bccc58ebe5489cc70628f635c1932fd371f5d7da850dbcf960f95f4c4afc/coverage-7.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:a678c0b6b22086ec2427359d22e37445d4a792f5fdbbc744112c7dade65cad02", size = 226372, upload-time = "2026-09-27T12:26:38.406Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f6/8eb4f220ef24f84fb27d852d4f9bf83e0c73ec1a4a08dd9a87e3f4529739/coverage-7.16.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a37c6e478cf687e1aa30a593d19c92c02fad9d122b51ab73f51b8dc7a0c0fc9", size = 223993, upload-time = "2026-09-27T12:26:40.164Z" }, + { url = "https://files.pythonhosted.org/packages/40/23/d4bbaf0c154e0b0c2b5264890dbf6ef098dcb50ec8f2469be9490d191660/coverage-7.16.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0993d0e90858c03943d3cb152e068a20dd4707924deec84dd2230261baae3b1b", size = 224368, upload-time = "2026-09-27T12:26:41.762Z" }, + { url = "https://files.pythonhosted.org/packages/7f/48/fc1e88fd571ec5cb38150b7f89f7696ca1bdf9920e01432febb69774cc85/coverage-7.16.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bb2fc905bbf4e6b7f40806ea79e31515abf6349594cdf0adf27c4215f0463204", size = 255358, upload-time = "2026-09-27T12:26:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/1d/56/6785397d07c29c8e70fbb9a07e97d062b43c21ffc5f12385917847f09f63/coverage-7.16.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4358b9c8c0125b460407f3017c6cce8156e904b32772c5630d27112f52bdbfe5", size = 257955, upload-time = "2026-09-27T12:26:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/27/3b/c8cdd07721e5f99abd81cea970d971997f99bf158c0b85f51bd284179c8b/coverage-7.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f15254427c9b33eedac4f198eaf9e356eb4f6214551afb43da6194a2c088ad7", size = 259183, upload-time = "2026-09-27T12:26:47.208Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/606b192fe43d32574ec6238549d48de588fdcc18485682a5ec0a8ac357f2/coverage-7.16.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9a75a4704ff640e46170042eec1f984385a121227c505d5a16ad8e495f452541", size = 261325, upload-time = "2026-09-27T12:26:49.084Z" }, + { url = "https://files.pythonhosted.org/packages/67/90/eea481f8b0305ceeb33f081a5f47e298391dbd1b589de0c4b3b3aa50d3f2/coverage-7.16.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:14253fc7bb15749b849795a06f5d3b6d8bc3fb8a4b5ddc341faf7a89dce205fc", size = 255531, upload-time = "2026-09-27T12:26:50.509Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/dedbf9aea1457b120c27ac10b8fc2a357f37fa2b54c3e7286d42980a0a2a/coverage-7.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:921415102a90637fcc2e3f169f61dad7699ecf690e8639fc21b813acbedc0967", size = 257329, upload-time = "2026-09-27T12:26:52.005Z" }, + { url = "https://files.pythonhosted.org/packages/fa/cb/b25c19d5bb2bd0f2e4e27fe8e2ffcae80c7a91ae181c0dc749ed60e9b1a4/coverage-7.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cce2bc991293f15cc4084ca116827b5900c5f34e1a54dfe83f10ab5c43162eb7", size = 255294, upload-time = "2026-09-27T12:26:53.634Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a2/892c5c5f4ad44b7b2ca009aee705191f3f268f15052244f2f9e3539b2e35/coverage-7.16.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1fa594c887365b69745f25a416806e61085dd07b94c9eae68a6e20730629b23", size = 259444, upload-time = "2026-09-27T12:26:55.243Z" }, + { url = "https://files.pythonhosted.org/packages/ed/99/a562537deba0a3e370182ae71c149be796c39d8087365f17a09188f27145/coverage-7.16.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11e597173af1dc33d5f8a7332ada544199269a223af1ee1770ddd5e245ad0fe8", size = 255111, upload-time = "2026-09-27T12:26:56.851Z" }, + { url = "https://files.pythonhosted.org/packages/2d/20/854ec68641a9b3362ff068a32dfa41637299761617ef253791dbade6fc76/coverage-7.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e7f99698ba3a7d13988bdd984b7ebf13af4dbe2166dc8502eef90d77603b0a4", size = 256882, upload-time = "2026-09-27T12:26:58.41Z" }, + { url = "https://files.pythonhosted.org/packages/db/0d/748e4518b0ac0f9ff2687c248a6e5f8c0737306e709372632a2556f84443/coverage-7.16.2-cp313-cp313-win32.whl", hash = "sha256:f80bd9f9633eafc73d0a913ba2645c96ba58bba1befc30590f7c0fbfde59d865", size = 226045, upload-time = "2026-09-27T12:26:59.983Z" }, + { url = "https://files.pythonhosted.org/packages/31/fa/6e46edba66a183fe4d99d4bb52c173287e9b8dddabe0888d24cb8210e580/coverage-7.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:8be099e979fc42559328a21828281b4578304191ae46ed4e80a407048a82eee6", size = 226581, upload-time = "2026-09-27T12:27:01.494Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d9/9ef6845367600b336ff75d000444a0d32497d6972c833141bd39356abf68/coverage-7.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:28ff850182a67d117990fa2ce5ea1032836d8c9630dae867e8bdd3bff4533b79", size = 226409, upload-time = "2026-09-27T12:27:03.116Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/577fc0803dab4155dcf808faffbdd7b159256781c0874a8586e17b81b149/coverage-7.16.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4ee546b9e4872ffa194bf07ac87bfa1202ebb824d0795dc1ef22f175545ca90a", size = 224138, upload-time = "2026-09-27T12:27:05.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/9e/e3785ba3ecba2bd11efc74bfe2801ca4b78c4480b15a375648d809a59da3/coverage-7.16.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a2fac6895eb299a2e52d7bbb8fb3903502b9da8d3f5309ceb16ec40c646b58ee", size = 224445, upload-time = "2026-09-27T12:27:06.805Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/963ff22d3fd27117da3b8cc442f5bdc91196f783321e1a8ff0ec43476772/coverage-7.16.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:57ff3783f99d75a1e81dd56a9737eb5665e6736a5d93258ba596b6dcad8fd05b", size = 256112, upload-time = "2026-09-27T12:27:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d4/a306940c81c6ae759e82fff27d20b7fdc6896e422b821f51313cce212b6c/coverage-7.16.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f37886699cb9abd29958247d718628d5bc6f39e623dff66a09e546c42a7e03", size = 258727, upload-time = "2026-09-27T12:27:09.927Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a3/d3d99d93b02517087aa05bc0cf2d04d372956b849e5443e059079901429b/coverage-7.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fd7a86fdda7cb6d616d178654bd0ad6bc0f3f33c2e478aa598500a1a9e34eda", size = 259909, upload-time = "2026-09-27T12:27:11.55Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/39dd599181726758dd185ae4dc0c0ab3aeabf7ca70e68e145060feeaaa16/coverage-7.16.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ac0f3b379c94acc2f7dce5f5f0b24d44fa1cc6a509717ef83dfee07450c2117c", size = 262481, upload-time = "2026-09-27T12:27:13.17Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/91ee43f6ded411460c359d7e1aebde4d6fd8f00a2e5394182d9d212eb23c/coverage-7.16.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d0732c83746bc24123c581a85d9dd96b70ddb538c9076020aa1a041790361e9", size = 256012, upload-time = "2026-09-27T12:27:14.91Z" }, + { url = "https://files.pythonhosted.org/packages/11/8c/e9499ddc33197bd7eabcb1118ca81756fc874457b324e2b479a4804b2ad2/coverage-7.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b451c68218c150f616bc9649783ec8de76a59792c759b43aa0c9c0466a465e4", size = 257898, upload-time = "2026-09-27T12:27:16.588Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6e/c081cb5991a0afba99f9c4ad6c74a5fce9513a38ddc64e3e6680c6fed9af/coverage-7.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a56ac4fa5a75c7e182e8f62600cfb4aff43c5ed7356a034f3557659c3bec1d90", size = 255938, upload-time = "2026-09-27T12:27:18.19Z" }, + { url = "https://files.pythonhosted.org/packages/b2/42/1c3d819e8f9b6eb01c2fe90874d67a8882adb9507e0bbb09361ed131ea89/coverage-7.16.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc4f73aa3fabc36e32046d6cd2971405948d8a903636508a3d3b2f9128b3a95", size = 260368, upload-time = "2026-09-27T12:27:19.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/4f/d70eac07901fd587b6ab05e659b52afe13959992aa5113bf6cce059cc572/coverage-7.16.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:723dcdab91357159b722935b500ee8abc0a66c8c432e1e9fabf4cc7598952de8", size = 255620, upload-time = "2026-09-27T12:27:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/6d87af88317d3d9a9b18a9ca1bc1673eb516917f296e579d0d4a55cb3490/coverage-7.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5397e21a90dde0e9c6896b77ded8f0be26b66f8b22b33aed41f6043ed95d55e6", size = 257521, upload-time = "2026-09-27T12:27:23.358Z" }, + { url = "https://files.pythonhosted.org/packages/79/bb/90c2641170d2fa1a6757b3f8450ba2740197317b0ddd749e9604b914e886/coverage-7.16.2-cp314-cp314-win32.whl", hash = "sha256:848893e1d361448c113dc2f0913503522a6f7be231d0e38333d2a22d9698a011", size = 226248, upload-time = "2026-09-27T12:27:25.153Z" }, + { url = "https://files.pythonhosted.org/packages/30/08/d8d0478bb02c8eb0ae20a496fc80c40fcf4d3450bd184300d682ba2d28a6/coverage-7.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a27b731c171e43dc8b5f32b76a5051dde2ec9b9366c87028f08a7088ebc2c7b", size = 226732, upload-time = "2026-09-27T12:27:26.907Z" }, + { url = "https://files.pythonhosted.org/packages/32/3f/0001da22155b0a8ce063ec0f7e64ecbe17b373f306e7a74435f6d6accb72/coverage-7.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:1c569a9fd25505f1cd6bea90588818f90373ce90e2632e2cacf19ddbd6e14fdb", size = 226645, upload-time = "2026-09-27T12:27:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/d7/85/6d8813aff9b8b8586691a9d33c43c5604f7227622574da7cdc3d91a86861/coverage-7.16.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d93db87adb6b1c1b408dce4763314b55d76a9f589e96783a84ac9e7689e48bdf", size = 224871, upload-time = "2026-09-27T12:27:30.32Z" }, + { url = "https://files.pythonhosted.org/packages/5c/70/444f3a4981ac2cda40fdcf4cc9b56a4e1a33c222abeb33e51ed3e3eb2a6b/coverage-7.16.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aa62c85046473959c13ba9edca9dc90a77d5c1095b1ba313556314d77fe5b036", size = 225123, upload-time = "2026-09-27T12:27:32.33Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c1/980681cd7b33eb66ac835044116ef0a92e11fcc7bdd866cc89d10b1130b9/coverage-7.16.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db76506aa5416081f3e8974ae0f7965c58ada0bb0ef7339ac86099588dbb20d3", size = 265049, upload-time = "2026-09-27T12:27:34.085Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e3/87679875c33bb2191f0f05544a1cc9adcc940fe0c35443a10f2df753dde5/coverage-7.16.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a0f2285329dac10ab08f79cb11f5692c497018e6c7c511f95e6fd63a70b8f831", size = 267717, upload-time = "2026-09-27T12:27:36.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/64/5d372776d6eb523d4e93bafba2253f96984e3b18261c4cc56a50863c6d0d/coverage-7.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:382d3346d56b0eec1b793d53a4c88799c8053f516aa3a8d7c44315696954bacf", size = 270047, upload-time = "2026-09-27T12:27:37.96Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/44082ff0cbf9f97d0043f57970a71204097ec7ba606361a9fd2065393669/coverage-7.16.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:648352b94507179d82637292e7ae8802508d95f78e2f00a705a50b6c48011681", size = 271360, upload-time = "2026-09-27T12:27:39.766Z" }, + { url = "https://files.pythonhosted.org/packages/b8/17/9a215efe25b5e0ecc87c89dbe525c4a87d14d87c8c0c7316ef140a5f6f3e/coverage-7.16.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb2bde05838fffae1a1bf75e5d411a6cac3e4e9bb97e6640fed8cd47888b33f0", size = 264736, upload-time = "2026-09-27T12:27:42.072Z" }, + { url = "https://files.pythonhosted.org/packages/a2/da/7f0a31af8e448107d4d32844bd684757f51ea907bc0c68c8fd537b2123ff/coverage-7.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6a75180829efb8ae62b4aded25be6ddca1c888d138d2d82e21d93bfbd88f41cb", size = 267600, upload-time = "2026-09-27T12:27:43.85Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/3bfecbd3366b775bacdcb3330394d356cf384b5d8f5b2146ac4b14b252b5/coverage-7.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99704f73721e23859112072d522076e11c31744fc96b5652e5dd2018aa4359f7", size = 264767, upload-time = "2026-09-27T12:27:45.768Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3f/5d62163732d87e4a0c4710a0eab30f0fd6a2d480112abe2029f014fe8c9d/coverage-7.16.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:29309ccc86b7f33df7db12813c299f215bbbc470ed6292d0bedd63ffae1ebf64", size = 269026, upload-time = "2026-09-27T12:27:47.787Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/8e4579f225426535085a9be371cc75e3b026d058d679b80affbdfb4c3ef0/coverage-7.16.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:30c1b65d529e46569899fadca59e4a87c1faf2886923f1307ba61e654d4f3c20", size = 264147, upload-time = "2026-09-27T12:27:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/d1/36/ef1f77e2c3f7bb03c2b13b9a2006f88700fdd75535ef158d70049f425c1c/coverage-7.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcf4bc2aab4e16b1c4c0c2005918f23a7dd5d7821ddae82caed9e3342dc2fcce", size = 266376, upload-time = "2026-09-27T12:27:51.551Z" }, + { url = "https://files.pythonhosted.org/packages/be/79/0cb2bf4428830dec971c718c2c841a039c084415c99e67281f5a72841aab/coverage-7.16.2-cp314-cp314t-win32.whl", hash = "sha256:a9cd3de0a5bfe7b0e21ee10e1a14e3d61bf52efc88217ab1d95d6ace6970bd46", size = 226585, upload-time = "2026-09-27T12:27:53.945Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f9/da17121c16667fd84998e972200ae226a41540f6ea4795776c6d99e8976f/coverage-7.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:611a44e5229a59d7483ce830160e1a0e85f700562c7a5651c7c63fb8f4eb528c", size = 227378, upload-time = "2026-09-27T12:27:55.778Z" }, + { url = "https://files.pythonhosted.org/packages/74/89/01179c62d1b7e6e33bd5001566b02d7f778cf33d3ec1e81e94ca170c517f/coverage-7.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:22957cef43ce038641de78ba995de7568d2d6a37c6ddbf7fa0fd7d1ae2344d91", size = 227064, upload-time = "2026-09-27T12:27:57.496Z" }, + { url = "https://files.pythonhosted.org/packages/4c/57/52935003c3f627ba6e5203d7179aad32448c10899663a30336aba8e81a2c/coverage-7.16.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:414c26dfdb96aac2d570a54e03008f001e32eb2d413705365503648c6bd361d8", size = 224140, upload-time = "2026-09-27T12:27:59.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/38/df472520f3e626524d7e2fc9d6da0afe7895a2f1489d36b48af8ca40bb41/coverage-7.16.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00d3eb96e9988c45f50cccd1f1496571ac5c1f91386ac02c4d55516eeda19a24", size = 224449, upload-time = "2026-09-27T12:28:01.299Z" }, + { url = "https://files.pythonhosted.org/packages/0c/aa/3be084d5b82e63ccdad4ed751e4acbae294673573e30481d29f8b7402eec/coverage-7.16.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dbbd1155ca46e6e0b6b89d204428c56ef6a459af21333f365d135a2820e5a09", size = 256060, upload-time = "2026-09-27T12:28:03.185Z" }, + { url = "https://files.pythonhosted.org/packages/de/29/48fca82a7ebf7ff7b2e35019cc9537e7f65e4d2aa1215cc5a8792c989251/coverage-7.16.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8fc15cc8d0d06e873c00ef18e1372d605f9aaf3de27d8c24e50782e75bc8b843", size = 259106, upload-time = "2026-09-27T12:28:05.15Z" }, + { url = "https://files.pythonhosted.org/packages/06/3d/b2d5986f2dd53fe201aa1be2e4ab204fa1aed5101e67c0dbbb419b850aee/coverage-7.16.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c6afdd69218202bc1758c9a14b86b8cf1084f37ed2ca143e567a103772b16d1", size = 260674, upload-time = "2026-09-27T12:28:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7e/b50160be3506ead12e6480d14279af7f0f17627694300a2d1fd2c42d2ff5/coverage-7.16.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aba5c63b7afdc749cc9eae943d5b868cba2b261a176378fa1c5a30bc8bc89982", size = 263139, upload-time = "2026-09-27T12:28:08.771Z" }, + { url = "https://files.pythonhosted.org/packages/14/5e/7c805ac9a32606de1399bd7e9bd375aa2f973dc61b12680d9e6403c2e891/coverage-7.16.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9174f0af24e5eff248b9dbfe76ec5275a3d19d37edbc2810543f12cf97347a34", size = 256591, upload-time = "2026-09-27T12:28:10.842Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9e/76f1ed129a2daf658a3ea17122824cf2e3b91fea0460d8d3664fc5a61018/coverage-7.16.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80e9fdb4c3d926b6ba721d4bf7435bdb869c3527ae7803290361d0ab73db13b6", size = 258708, upload-time = "2026-09-27T12:28:12.962Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b7/8d62e75f48b527619239a65294f842d4b7fd02a0839d43ae1de80184e2df/coverage-7.16.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:7b3bce4a0d05401d70b7d0d5ca783e686bc9d30e81dbd7d980d532609bf809e4", size = 256562, upload-time = "2026-09-27T12:28:14.934Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8d/0a15f95c3afb78e947c52644786ba4bc9de259905687dd720d5e6fae2e76/coverage-7.16.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:44f21e407b278efdfc1ee5e481e00518bd1d500310a30a5fbf2bcbedfef4aaf0", size = 261077, upload-time = "2026-09-27T12:28:17.215Z" }, + { url = "https://files.pythonhosted.org/packages/25/00/88389987305a47d732866c07c8a500000ab574df9505e3114ac69c8d027f/coverage-7.16.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:59c3926585e1cd1f2190f4b2ac9014de1bbeaf0d5d0587b0dc6b0aa90d17896a", size = 256034, upload-time = "2026-09-27T12:28:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/92/02/34d079d4952ad461bde037d353f9a6e037a7edc45fe0f9ee8781ff73f028/coverage-7.16.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:066429634299e14dd2d511e1e85f8f9cecc500781f6b41907c0dd6f1baea7e63", size = 258034, upload-time = "2026-09-27T12:28:21.242Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d8/3e59a62879285b464ec1b10fd824fbc1af9ce66e842cd39974f80a0becc4/coverage-7.16.2-cp315-cp315-win32.whl", hash = "sha256:893ea9cf86cb8d2546812ac93d973aaf2ee1fb45110a873b014214fd23e3725e", size = 226252, upload-time = "2026-09-27T12:28:23.102Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e1/128026e1b2836e9ad6b219207ba9edf1c5e0088a7869e23088aee7fbbe7a/coverage-7.16.2-cp315-cp315-win_amd64.whl", hash = "sha256:01c6908bc613b420c26c818fe948e1b97dfd041a53c98b01c63bd8321f5c9aae", size = 226728, upload-time = "2026-09-27T12:28:25.21Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f4/c9fa8e7cf525ca7748ac52b0ee89331d13fe09808e45c679830708782e90/coverage-7.16.2-cp315-cp315-win_arm64.whl", hash = "sha256:967d72c835d7a8cf0af99ec813a2d06e3db6df706402f1fe85b31b437645f495", size = 226648, upload-time = "2026-09-27T12:28:27.136Z" }, + { url = "https://files.pythonhosted.org/packages/a2/13/e96b045447a856666f36f9c653e2a80bdaa732aaaf72412b19aa2c26a473/coverage-7.16.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:98d9c97f51b334b0adce7b964442a9af33c1a00c6ac856984cc5dc8d18f81c75", size = 224858, upload-time = "2026-09-27T12:28:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/23/90/087f6ad1bd3df059632ca3407a4e6552ed1053ee35354de0a771acf35423/coverage-7.16.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e861f1071dcc2fec1e88bef0920f6b1eaa66a143555b4f8ab79ba2b0f30ef55", size = 225139, upload-time = "2026-09-27T12:28:31.131Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8e/285dcef0184358044e7cbcd810a1bdc9566bc620f54702d605477155df4a/coverage-7.16.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb9d92ecfe2d5b494367c67f7446f8b75b68d8d0c8cf3bc3e6997478be25d9e2", size = 265090, upload-time = "2026-09-27T12:28:33.04Z" }, + { url = "https://files.pythonhosted.org/packages/06/b2/cc83f3a6e5789a4e89059c69555bc641c2efcde568405a1c06fc702951ab/coverage-7.16.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb57acff4a74246ae513c142d4b36e18c389c3aed8661914a53f7cd0071031b2", size = 268254, upload-time = "2026-09-27T12:28:35.135Z" }, + { url = "https://files.pythonhosted.org/packages/ac/41/f548c19530f5d66ac6e3c92bbcbc49da7261de3a458b9f3e54a3efb1a0b2/coverage-7.16.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:444889f7f66b74e4455c0a97e0e166dd41177f1dca8c0239a47cff25e05ba7e1", size = 270695, upload-time = "2026-09-27T12:28:36.959Z" }, + { url = "https://files.pythonhosted.org/packages/94/61/4dc27cf82ef96434d2874110ad0cc10ea4621025705dc5049862bd3bd181/coverage-7.16.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a740ea6f083c6db7b926534d159508f80ba275ab35e722522de0d18d0f56e55f", size = 271853, upload-time = "2026-09-27T12:28:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/38/29/bf8072b1b8bd5f2de8b21460a404460b1a2b97e80a9464c78ec0271f6199/coverage-7.16.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e209591f7c41ae4a9171335cf6156afda0b21de73b02f73f5aa95b2d5fbb08d", size = 265644, upload-time = "2026-09-27T12:28:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/0aecb8721be5cdeb8afd9d6d9f6b463f074e4d8d37f00f4c42442522709f/coverage-7.16.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:396bb16e04ce04efbb3df91456ae4e3da918e69ecdf67fb711b0a0fdf35ccce0", size = 268530, upload-time = "2026-09-27T12:28:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0b/92b4b7628268ee711249958e68fc0328779bd3d9a7ab4715379465aedb84/coverage-7.16.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9cdf19874e0d247f32f03609200370343c3c7aa260b191d8c2bb251d36198283", size = 265154, upload-time = "2026-09-27T12:28:44.684Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/41c95c1ab29b3dcd357cd1227181d1c98185632aca41ce670ce671b23a43/coverage-7.16.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:fd3d72233eb8b48acc94fa57d44e2d32ce8e7abed02882ccb6d855ccc4ed33ec", size = 269799, upload-time = "2026-09-27T12:28:46.672Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ebeb259aa5362b033a137b86d7274ff4b109d59be8cc9913889b783bf75a/coverage-7.16.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:bb4ffe96aa663cee727659db5a2afeb38c95f8677b747d447b90d6d4874ea2c5", size = 265196, upload-time = "2026-09-27T12:28:48.996Z" }, + { url = "https://files.pythonhosted.org/packages/b2/18/8437620f90d023680a072eee02f968055f3658bbfb7d386d0ea34cfb7f30/coverage-7.16.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dba2edfb054f6d4a08df9d1637c39a5aa3865bca6617c13c86be21e45658a59c", size = 267266, upload-time = "2026-09-27T12:28:51.361Z" }, + { url = "https://files.pythonhosted.org/packages/28/6c/f08e8ee4293e6434035424180bef4d45e028e8ecc006c61bf9453e74405e/coverage-7.16.2-cp315-cp315t-win32.whl", hash = "sha256:251aed777c47c77aba047096d4542889db089227655711dfc2b9c54ef0e15e35", size = 226583, upload-time = "2026-09-27T12:28:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/f7/fd/3f939c2847f4a72c20cff8b1ac33da78ea91a2d38d9b43336e60db719103/coverage-7.16.2-cp315-cp315t-win_amd64.whl", hash = "sha256:2aca0bdfa9e91621d5b09d815357bf63def4fc0e9cb66da67bf2cf93f3b1a6f5", size = 227374, upload-time = "2026-09-27T12:28:55.158Z" }, + { url = "https://files.pythonhosted.org/packages/5a/35/b98cdc354c952402132e675a87f2cc3227fb68f959c84aaa491fbe15933d/coverage-7.16.2-cp315-cp315t-win_arm64.whl", hash = "sha256:b88841e654f09732804809e435b3e005a929ffd9998b872b7b213957b8759cb8", size = 227064, upload-time = "2026-09-27T12:28:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/7a64e1ac90541a8edf50daef0914848011fb057a5bf55284a4811e21939a/coverage-7.16.2-py3-none-any.whl", hash = "sha256:11d28e9123a9156cb405d8d27b44256c9a58fb5decc2073a8f17862057e3aa0f", size = 215754, upload-time = "2026-09-27T12:28:59.075Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "50.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" }, + { url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" }, + { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" }, + { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" }, + { url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" }, + { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" }, + { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7a/f08d34ce09d60f89ebd391e2ebc6ba2b995e6dd7552f41820f8085f94e53/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:92e665960f25fcdc73725b9cec7a3824f279ba97a98653afe9ffac2e43668f67", size = 4716035, upload-time = "2026-09-30T15:29:48.681Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/e18fb65592451a2acb76e9f2fbe14e0f47a8318b4c5430f1633851d03daa/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eef4c2f3423810b3070ab391f85436d2f8bbfcb286ac15cbc73190b3563b1f1a", size = 4726917, upload-time = "2026-09-30T15:29:50.608Z" }, + { url = "https://files.pythonhosted.org/packages/83/28/38fdce17e60f6b825e69fc3b7f75e70a6612759980704697e1de4cbfaf6e/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7c6d0330c472d96f6a6afe24d80dfdf15176c33096f0a4397ae4c60f3dd3be48", size = 4715341, upload-time = "2026-09-30T15:29:52.522Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/d9121a717e0f893c64bd6ca7702614778d7df2a5c309128a002421788516/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:1ba34f04897fcdaa73f74145c25f3ec146fbd56593853e88adc2e811303c5f42", size = 4726322, upload-time = "2026-09-30T15:29:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/e6d153808bf353e152abd2fd4d8f09670d956ac78379ac46e60d7efbf04c/cryptography-50.0.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:3dc4fd8058cea1644971207d530e1a03a184a805ffc8ebdddf0599d78a331b81", size = 3873097, upload-time = "2026-09-30T15:29:56.097Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1d/1271f287ff7170ddafc2aad36260c4eec20ccd2fea70f38455e9d56d427b/cryptography-50.0.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:7b75de3c8b3be1cdb1052747c929440c3eea46c1bc2cb8a6e3a48388e9b7b452", size = 3805376, upload-time = "2026-09-30T15:29:58.729Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "jorisjonkers-commons" +version = "0.0.0" +source = { editable = "." } +dependencies = [ + { name = "cryptography" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "cryptography", specifier = ">=44" }] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[[package]] +name = "librt" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/0e/b04fd2d76b6a78a02286938251b2c119fb36ddece92c76cb4886ef7d44b8/librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18", size = 148058, upload-time = "2026-09-29T00:44:40.903Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fc/e1ed24aadb32d38603c0c07f579d739eb4d90e64e90d525b024e4a999b4d/librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421", size = 153073, upload-time = "2026-09-29T00:44:42.165Z" }, + { url = "https://files.pythonhosted.org/packages/be/38/133ab655409420e0c34a9a9836a7f4e5ae75fba758d7fe177099130d58ba/librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f", size = 493052, upload-time = "2026-09-29T00:44:44.043Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/aaa663bc9421f5bf7bbbe9d3316d7219749698ec7688ac3b519770a89c04/librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b", size = 485506, upload-time = "2026-09-29T00:44:45.479Z" }, + { url = "https://files.pythonhosted.org/packages/7b/59/83a6eb5087ba8b45b083859e2085256963e06d4c2531944cdd962670bfb5/librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071", size = 498708, upload-time = "2026-09-29T00:44:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/15a8d62b2bc12935080ec7277e2e0b9b233047781eb784a51d3e25af669a/librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602", size = 515981, upload-time = "2026-09-29T00:44:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/4f/287cc282ac5dd815204c0779e79401bb0a568c3a1335fb319bf0838fb566/librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d", size = 508670, upload-time = "2026-09-29T00:44:49.726Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5b/ab7292add898ade0d2b5b75d32a923aae315861671aa5c39f5491309f7ae/librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e", size = 530393, upload-time = "2026-09-29T00:44:51.356Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/8e6bcf027d4bcbc16a4e864b3d158a1d3e5f55ffa53af0238020cad89271/librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1", size = 534302, upload-time = "2026-09-29T00:44:52.893Z" }, + { url = "https://files.pythonhosted.org/packages/96/a4/8c5ab4f3b3a8e905ff8305d76976a2e666f52fe049edd398aed2402beb36/librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905", size = 536150, upload-time = "2026-09-29T00:44:54.421Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5e/7c4bc267e3cd4b8bd288de9f62d61fff203829c9cc649ea2aa12e9218cf0/librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e", size = 514316, upload-time = "2026-09-29T00:44:55.998Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/c8bc6b50f43c73819334233f5a3259c81216742cef56d2140c3dd377c3f0/librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96", size = 557731, upload-time = "2026-09-29T00:44:57.581Z" }, + { url = "https://files.pythonhosted.org/packages/93/fb/e241f91bc8de6c7ac1c6a20ea6dd381bc52312b44d711ffcd97140db1e57/librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7", size = 104922, upload-time = "2026-09-29T00:44:59.078Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/c892383d2bb1a6a80b2ec8ede84970a265a792c56614a06282bf3008517c/librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d", size = 125093, upload-time = "2026-09-29T00:49:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/8bbde1ed0b21480163617648f638fdaec9374cf5e9c846ebcee34ec02a42/librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678", size = 115910, upload-time = "2026-09-29T00:52:40.131Z" }, + { url = "https://files.pythonhosted.org/packages/ad/76/bbdaeb87b7c47b5c7343e90222b9bfa4e4a8a83f647e02933ad0129225b1/librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", size = 151028, upload-time = "2026-09-29T00:52:41.508Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0c/ab8ed3dab0085931aec4a792c7eaac8dc6c5ff4691fda3a5360d9d8a9cd2/librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", size = 155283, upload-time = "2026-09-29T00:52:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/eb/36/494e79d460c80c1f030661e8287c9eca5e1ad652dc2b2180b6cd42abce0a/librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f", size = 503185, upload-time = "2026-09-29T00:52:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/9b/34/a8464038dd9db6e4381fa2b6eb73dc9a50888d77102c4c139304ac35cddc/librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", size = 496738, upload-time = "2026-09-29T00:52:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/6a/53/e0e5e334ef0c6ed27039d323819368b9ef6712be87d55ee2bf9799398afd/librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", size = 513818, upload-time = "2026-09-29T00:52:48.068Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f7/7ce72cbf19d0addd05090b339152fd0548a02562c2866a603e6e3b3da2df/librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", size = 531952, upload-time = "2026-09-29T00:52:49.816Z" }, + { url = "https://files.pythonhosted.org/packages/83/22/0b1bcb6a8e723c8b4fd60dfc8ae8ec6461c54073fbc8685efeb8d900d407/librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", size = 524465, upload-time = "2026-09-29T00:52:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/64/2e/e9c23b8b9df1813da1be205deca9606beb7ddd033972246cd426d05374a0/librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", size = 543191, upload-time = "2026-09-29T00:52:53.284Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e5/6a8b21b342c03ed7e230fa3afbfd2edc58e6e87ef1f0d11fa2b9a748c252/librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", size = 546979, upload-time = "2026-09-29T00:52:54.932Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/b5129023eced1be01e01c22757f53be551d463b1bb7264f787927404c1d6/librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", size = 555317, upload-time = "2026-09-29T00:52:56.869Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/28bba5938c725fe91f06bf93f7fa6c6b150229df53a87b454b0d5c2a796e/librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", size = 535846, upload-time = "2026-09-29T00:52:58.475Z" }, + { url = "https://files.pythonhosted.org/packages/22/92/63773026614f888c5d4e370e395ce42ca604b89f70b3acdfedbf94851b80/librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", size = 573700, upload-time = "2026-09-29T00:53:00.127Z" }, + { url = "https://files.pythonhosted.org/packages/66/8f/347d4677eefb57cd9f8e01d95a1dcee9a91b4e44e664173c32ff6f3752e5/librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", size = 106158, upload-time = "2026-09-29T00:53:02.031Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2c/5193dc81127cd5ddfad031391b046bf32dda219b38463ab872407ca30646/librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", size = 126176, upload-time = "2026-09-29T00:53:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3d/9668a400c8dd81d162eba38b33fa49fa6205f1a64493570a13fbb815c3ee/librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", size = 116572, upload-time = "2026-09-29T00:53:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/ae5e0e9dba45d1399aa04a5395bcc0bead40d9fa06dc903634a7b4d7473d/librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a", size = 151036, upload-time = "2026-09-29T00:53:06.284Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/48a16e323c5f9447a94cce7b59babf60fa04e62c3365ecf060c77ed8b320/librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc", size = 155211, upload-time = "2026-09-29T00:53:07.72Z" }, + { url = "https://files.pythonhosted.org/packages/3f/29/0f59299eb4251a409b2e690ad4b7d9f8a676db829d7817ec961f32b44f7e/librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32", size = 502587, upload-time = "2026-09-29T00:53:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/de/ba/d6fb4ef8d1537c396079d72289f16be7cd35a366e5065c51253fea2760b6/librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e", size = 496146, upload-time = "2026-09-29T00:53:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/52/fc/8c50dd4d7cc97c0ee8f252c8a3104980f234391cf1519b554e8b9de08b60/librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087", size = 513336, upload-time = "2026-09-29T00:53:12.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/16c409c56f708eda2db9a0553662845d45e3871c77d70a240dae3f3bdc56/librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6", size = 531689, upload-time = "2026-09-29T00:53:14.39Z" }, + { url = "https://files.pythonhosted.org/packages/88/82/d34772a6c29d1446dcca6e64d74062efd508523ab351aa16625a4689d5cc/librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659", size = 524537, upload-time = "2026-09-29T00:53:16.064Z" }, + { url = "https://files.pythonhosted.org/packages/77/8f/24c5631313746131ccee53bc91fdc8374f9cf25e0082a1fee9c93bb98acc/librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd", size = 543231, upload-time = "2026-09-29T00:53:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/f2/cb/5f8e0d41dbd8b499c2265e939c31acc9ba59845565bf99539ad1c06aebcf/librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5", size = 546499, upload-time = "2026-09-29T00:53:19.666Z" }, + { url = "https://files.pythonhosted.org/packages/be/61/063052de441d1385f59cea4223f184bf9e5d125de1ae3239b490aa1e486e/librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63", size = 555261, upload-time = "2026-09-29T00:53:21.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/11/a2ada0529372268d6401afa9d457a095b68cd7753532b6f7f33049a19b43/librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef", size = 536029, upload-time = "2026-09-29T00:53:23.07Z" }, + { url = "https://files.pythonhosted.org/packages/23/9d/5bb6d38853382986dca702fc7e06c8256d30f5fa0676d882773b744610dc/librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e", size = 573748, upload-time = "2026-09-29T00:53:24.696Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/0025cde35ff7f607684dc775a2b2d732cce2561cec050a6ee1fb2e1fc6fe/librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d", size = 79861, upload-time = "2026-09-29T00:53:26.089Z" }, + { url = "https://files.pythonhosted.org/packages/93/93/303b8592909bd583f83f02818ffbea3f7647ca1e22b1cd5465d04b8145fe/librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4", size = 106236, upload-time = "2026-09-29T00:53:27.43Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/80bbb463c60ed18e29cb26aba386ddb76609580e4e7160710e550587018c/librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92", size = 126180, upload-time = "2026-09-29T00:53:28.7Z" }, + { url = "https://files.pythonhosted.org/packages/43/80/b1a6fbdd7da825cdd55c71aa81eb6cfa82c513c360774152eacb50b4a771/librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91", size = 116557, upload-time = "2026-09-29T00:53:30.012Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" }, + { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" }, + { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" }, + { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" }, + { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" }, + { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" }, + { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" }, + { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" }, + { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" }, + { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" }, + { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" }, + { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" }, + { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" }, + { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" }, +] + +[[package]] +name = "mypy" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/67/e53965e67d23d48e78dd603aa7525a8d77e7818c23954cc217ccde5cd5c0/mypy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d20e6c7c35fcbf2a0ebdd0eaeacfbc243009dfd33ab7822d54e213912e6dbbd", size = 14315745, upload-time = "2026-10-01T20:39:25.03Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5d/a588ccd57625822d8484d7aff4e9c70f88d042a91c2ec84324a2bc14b3f3/mypy-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3011537be6cf1de4511c0255a324362a812b58184bbe61e15f59c8b31033bd74", size = 14465652, upload-time = "2026-10-01T20:38:57.19Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/b4978a5566cb829ca563dc08e3ad8d944f116078786ad54ba3dff2bcb64d/mypy-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a96b07a49b7b1d025ce59c1b3acbcf24bead9a83da4523c4a6bde1bb94e7a0e1", size = 15419180, upload-time = "2026-10-01T20:40:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8c/52c71f31c54d0a749d832cc73b933c6e38c52d9b270d5503e031c73723dc/mypy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c4f8f8d1d1c0e2832d8ee7113dd08f6df6c7aad9e863fcbed9f25832be0b8c4", size = 15669212, upload-time = "2026-10-01T20:39:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/e8/55/ba63121494404fd7d3787ec176ccd02820d6ec125ac4e7c4c0b79f2cc6c7/mypy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba05652540bf12828e52abae807b024b09ca144ff4f75e2450a81d69c376425b", size = 11299572, upload-time = "2026-10-01T20:40:03.627Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9c/385c81b5c0d3a2917f077c4da660d0493f9521ccfd4b058e33b860f72d02/mypy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:6306086b87cf7f8a29aa618d9fd9bffb56c59247166b9660fdb54d86d7714ecd", size = 10869751, upload-time = "2026-10-01T20:40:25.483Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/e5d7cf4017e74a1c1e1c4058ce8f614fc1e3e7606564f47166e22bfc9f95/mypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e05ff2925d8b37ad26c80c1b9dc43ae5d455da2df1e23c24c095a6425917c57e", size = 14378593, upload-time = "2026-10-01T20:38:48.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/7d/12d994886a922f0f1997becc9c6625198d61eeb5962558a886af7dd38d54/mypy-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29243242cf72582b65f9582ad9e56e8cb281566ed3519f4cd70bb8b9f2977e90", size = 14464697, upload-time = "2026-10-01T20:39:11.059Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9e/bcc9af755425ad17790bf11d73c2ea7592914cf309ee1340997670f9d57a/mypy-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29eb0b9427a6b11b992e452f6cceb8af724f4dceb47e779d0b35e405e996ea5e", size = 15552257, upload-time = "2026-10-01T20:39:56.269Z" }, + { url = "https://files.pythonhosted.org/packages/af/0c/3343fc4525d6f00d75ad17a93a9f052d5163641cb8911840d4a12cb59ff2/mypy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebe2f72a2a1156065a9851570ffbf50c0a93cdccadef9c6e05c508a4fd10b1", size = 15800443, upload-time = "2026-10-01T20:38:54.815Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0e/69aac6b8159da7c53e6115a2be1bf48e85503402cadbf506dd708e8b2ad7/mypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:236e0d68f6941992b0811128e652590f590db444ab29ad8f1324765b9298b946", size = 11381574, upload-time = "2026-10-01T20:39:49.406Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d3/d32ce4feb5993eec09d2024bef16cedc9b93701b1446b86092f08b24491b/mypy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:82d0f94c8587ccb472622ee7795280aaa38a06640d5f45b3f16909d6dd86a989", size = 10944729, upload-time = "2026-10-01T20:40:15.203Z" }, + { url = "https://files.pythonhosted.org/packages/44/f2/eb15183c97c69d7cbfac990a6efd33a19ecfd97dab9e714c742fa78a784f/mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8", size = 14213395, upload-time = "2026-10-01T20:40:21.178Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b5/ba91b6ff65e4d6b6ff53b2b3b3ac5f1babf0c7c27d0b43a0196b1c967926/mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324", size = 14373009, upload-time = "2026-10-01T20:39:58.86Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c4/484275efc935c0003e55e4e8a33e4b8e99528ee956c12256ece4708f903f/mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb", size = 15465557, upload-time = "2026-10-01T20:39:01.794Z" }, + { url = "https://files.pythonhosted.org/packages/50/30/66eb6fdd0875e3c9025a02f0bb0ea2e524b74274b658c37fde0068c4939d/mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1", size = 15728673, upload-time = "2026-10-01T20:38:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/58/bc/2aa98fd7f49c42dba8e9c065886fadffdd00f3c654cff3f2a7103a797ac8/mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf", size = 11389803, upload-time = "2026-10-01T20:39:37.547Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/17b60df2d946792ef6af43b89351f5c9ddabc69053206b2304945572a744/mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303", size = 10951060, upload-time = "2026-10-01T20:39:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" }, + { url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" }, + { url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" }, + { url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" }, + { url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" }, + { url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" }, + { url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" }, + { url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, +] + +[[package]] +name = "pycparser" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc", size = 113796, upload-time = "2026-10-09T12:56:59.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80", size = 51178, upload-time = "2026-10-09T12:56:58.131Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "ruff" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" }, + { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" }, + { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" }, +] + +[[package]] +name = "tomli" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6", size = 17662, upload-time = "2026-10-07T12:23:37.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/ab99b60ee52acd949684febabc3005d0045d0f66bebd9cdebd67372d26dd/tomli-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545", size = 163901, upload-time = "2026-10-07T12:22:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/bc/00/ee01b7ed4579180fff07142d290257f25ba786f23f3ec6005f620933c2f5/tomli-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef", size = 163756, upload-time = "2026-10-07T12:22:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/72/c2/4efebf65372f6583185f79799312109dddb61102d47e5c33dcfd1a297aca/tomli-2.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b", size = 268038, upload-time = "2026-10-07T12:22:18.135Z" }, + { url = "https://files.pythonhosted.org/packages/53/07/5850468e925d898abb36038666f9c333a94d2a223e802a8ba5b6d319d23f/tomli-2.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56", size = 276422, upload-time = "2026-10-07T12:22:19.567Z" }, + { url = "https://files.pythonhosted.org/packages/b4/87/f293984cdcf83c054196d4fd3dad44fc68ae55b4b8c44bc76cef360c3150/tomli-2.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1", size = 272616, upload-time = "2026-10-07T12:22:20.794Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ce/db582886b3c1219d3fec93ebd669332482e5aee7a91e0f7838d84f2d1759/tomli-2.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885", size = 276593, upload-time = "2026-10-07T12:22:22.12Z" }, + { url = "https://files.pythonhosted.org/packages/bf/72/7619b87dea4261fc27dd7b54c4461c129c1f7d9bb7ba3aec89c797a431b8/tomli-2.5.0-cp311-cp311-win32.whl", hash = "sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e", size = 101830, upload-time = "2026-10-07T12:22:23.651Z" }, + { url = "https://files.pythonhosted.org/packages/1e/74/220106da34502304b6751a2a9b8a9fbca6c3fd47e737a2e2e3da7c61c9db/tomli-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8", size = 112742, upload-time = "2026-10-07T12:22:24.972Z" }, + { url = "https://files.pythonhosted.org/packages/27/99/7d9c8b41837a7773613e169504147375c157a290167aa59ad74a085f521f/tomli-2.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980", size = 109332, upload-time = "2026-10-07T12:22:26.117Z" }, + { url = "https://files.pythonhosted.org/packages/52/ed/7baa86f87493646a594de388c7c1c40a39dd0461f7e9c0359cbeefc91fe8/tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df", size = 164854, upload-time = "2026-10-07T12:22:27.444Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b1/44c0341f2224397855723c7a8a39f718ea6fcbcc3dacc66e5aeca0f334e3/tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b", size = 164074, upload-time = "2026-10-07T12:22:28.679Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e2d5b7d3fba47adedb23de616c16d428ea076c79a3d8e1d95d649ffe197e/tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0", size = 274274, upload-time = "2026-10-07T12:22:29.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/90/6090e706ff27a6f89f4a40578e3324b95c3cd8c4150868aabf33a8f414c3/tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6", size = 286435, upload-time = "2026-10-07T12:22:31.297Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a2c40768df16c408f22430afb0a73e9d7e5f79c950884954649d1146b74d/tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc", size = 278119, upload-time = "2026-10-07T12:22:32.601Z" }, + { url = "https://files.pythonhosted.org/packages/12/25/3c0cb485b98e9cfac495629b1c93c87ccf0b72fbe9d2689fd8fe62c6d5a3/tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7", size = 286177, upload-time = "2026-10-07T12:22:33.745Z" }, + { url = "https://files.pythonhosted.org/packages/77/8b/0144c65f0e37e51c18d04ae15c21b19431c165002d0131fe9aa8b0b8b1e8/tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2", size = 102760, upload-time = "2026-10-07T12:22:34.887Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/5d6d8f42fc9a05fce69354e00ff256484192f5f2fc9a2165718fa0de61ec/tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7", size = 112722, upload-time = "2026-10-07T12:22:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/65/df18032218db0fb9b769fb23c8039a051f15c811993995ea04c350273a32/tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea", size = 109534, upload-time = "2026-10-07T12:22:37.296Z" }, + { url = "https://files.pythonhosted.org/packages/42/e5/51736d70da209350969e15aca5c5ab6e2ce1ea87a0a892a6c13aec172a86/tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea", size = 163328, upload-time = "2026-10-07T12:22:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/086f80dab4ab497602644274e6dea7ec5dd0b4e262e443a8ad3bb7edee2d/tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043", size = 162246, upload-time = "2026-10-07T12:22:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/aa/eb/3ecc94459f3635c92321f4e7bde571323fdb2267c50e19e3188a281eae3b/tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0", size = 272655, upload-time = "2026-10-07T12:22:41.08Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d7/494fd1f0c37a621f1ad9975c2efadb523e8101f144ed6edb2e7fe64738f2/tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b", size = 283595, upload-time = "2026-10-07T12:22:42.222Z" }, + { url = "https://files.pythonhosted.org/packages/70/51/bb8d62b1317e6640866f6949b2d5855e5300f2c99d46de1cd245570bba65/tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066", size = 276253, upload-time = "2026-10-07T12:22:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/f4/f46bd7f0763cd47de2db697dca9257c6a4adfd1a93b018cc75c8190ed5a8/tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b", size = 283582, upload-time = "2026-10-07T12:22:44.983Z" }, + { url = "https://files.pythonhosted.org/packages/ac/03/70f2bcb2923a6db37818d917e124270a7f4cfd38ea576f5aa753a91c0ef5/tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68", size = 102628, upload-time = "2026-10-07T12:22:46.508Z" }, + { url = "https://files.pythonhosted.org/packages/dc/98/d52024bb5b0ff68b4f0d276d867f634c84a67319a7e9f6b7708a37742333/tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc", size = 113301, upload-time = "2026-10-07T12:22:47.647Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f2/540db3a70572a8c23a28aba3e9c358ce0ffffbafc990905c1343aa265b31/tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84", size = 109744, upload-time = "2026-10-07T12:22:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/caf6b307766eb9567664a8707e9d6be5fcc0e8903f18781c6677a60d80c7/tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105", size = 162899, upload-time = "2026-10-07T12:22:50.088Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/68cfce773a2733a49c74f99d627fb461bd990756860099eac25617889585/tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646", size = 162080, upload-time = "2026-10-07T12:22:51.558Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b2/e5bb8651fdad593f670501a7d718b1a7f73f064d44dea15e04c04dfef45d/tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b", size = 273380, upload-time = "2026-10-07T12:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d2/9e2d7f8b1dfe0e2b34c245986ebd55c4c553ea4ce6c47c443b332673253f/tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75", size = 283228, upload-time = "2026-10-07T12:22:54.173Z" }, + { url = "https://files.pythonhosted.org/packages/ba/df/ec7b876b7b1a2718bd74a3743c076fff565b04029ba33e8f61fac262739f/tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb", size = 277189, upload-time = "2026-10-07T12:22:55.342Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7b/e192d9eed0b9cb80da799f4d77052297fb9a2c3cc9b19f571f56ea88add6/tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3", size = 283632, upload-time = "2026-10-07T12:22:56.735Z" }, + { url = "https://files.pythonhosted.org/packages/84/50/ff94454e75461d75623e47401ed323d65c10aab8fe9033242c20cd2fdf32/tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b", size = 103535, upload-time = "2026-10-07T12:22:58.084Z" }, + { url = "https://files.pythonhosted.org/packages/54/0b/bdacf05f963bd6026ebf6eeb0beda847d1d60e03e440725c64a4e08a0afd/tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a", size = 114621, upload-time = "2026-10-07T12:22:59.2Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/53f438fa6ae4f9d4ed0ddde3e7242b3bdc34b48c8f9948b72b9e9b127676/tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3", size = 111572, upload-time = "2026-10-07T12:23:00.479Z" }, + { url = "https://files.pythonhosted.org/packages/b9/20/1f88f19427d380a40e90a770e087489eaafe4aeee070ae88ed2bbec00acd/tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4", size = 171814, upload-time = "2026-10-07T12:23:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/cbe5079c9f9a54b9b3e27fc82f08f3cb36edee75561679f53d2380c801d6/tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d", size = 171324, upload-time = "2026-10-07T12:23:03.18Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/1d53fd3b0f1cb3ba542e345ec32c26aefdddc4e829e4f3429af8a4f27782/tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9", size = 297441, upload-time = "2026-10-07T12:23:04.345Z" }, + { url = "https://files.pythonhosted.org/packages/66/d9/0800acb6a111686f764c1b91ef15cc42a20a66a46013bb42220f1d2c61c1/tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f", size = 307476, upload-time = "2026-10-07T12:23:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/30a8f3cd51b5bec37f04744bad0b0dc6160df84aad4f27b0e9283d66f221/tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374", size = 296113, upload-time = "2026-10-07T12:23:07.202Z" }, + { url = "https://files.pythonhosted.org/packages/ab/18/0b9ffc597e69c5a1e20a7823cb60d54b39a9f54e91edcb8574f022186758/tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442", size = 307725, upload-time = "2026-10-07T12:23:08.508Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c7/18f8baae0b5607a60e8e19b4a7fedee43a8ff6458e3896dcbbadeeac9c22/tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03", size = 108546, upload-time = "2026-10-07T12:23:09.956Z" }, + { url = "https://files.pythonhosted.org/packages/72/34/4cca9739254130627bde87500b3f2b512154fe2f278efa7e2a5e10ad4bcb/tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1", size = 117814, upload-time = "2026-10-07T12:23:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/afa530d47dd80a78fce43beac6bc6e00f84558eafcffbc6f37b21e80d056/tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0", size = 115188, upload-time = "2026-10-07T12:23:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc", size = 162775, upload-time = "2026-10-07T12:23:13.941Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276", size = 161406, upload-time = "2026-10-07T12:23:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52", size = 273855, upload-time = "2026-10-07T12:23:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7", size = 284910, upload-time = "2026-10-07T12:23:18.166Z" }, + { url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391", size = 277723, upload-time = "2026-10-07T12:23:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859", size = 285115, upload-time = "2026-10-07T12:23:20.698Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb", size = 103475, upload-time = "2026-10-07T12:23:21.941Z" }, + { url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5", size = 114589, upload-time = "2026-10-07T12:23:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd", size = 111493, upload-time = "2026-10-07T12:23:24.233Z" }, + { url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57", size = 171380, upload-time = "2026-10-07T12:23:25.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd", size = 170553, upload-time = "2026-10-07T12:23:26.855Z" }, + { url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01", size = 294428, upload-time = "2026-10-07T12:23:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f", size = 304909, upload-time = "2026-10-07T12:23:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a", size = 293220, upload-time = "2026-10-07T12:23:30.608Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142", size = 305705, upload-time = "2026-10-07T12:23:32.181Z" }, + { url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5", size = 108432, upload-time = "2026-10-07T12:23:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571", size = 117281, upload-time = "2026-10-07T12:23:34.648Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7", size = 115069, upload-time = "2026-10-07T12:23:35.77Z" }, + { url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b", size = 14765, upload-time = "2026-10-07T12:23:36.875Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] From 066b50de2244751db00260dfbae09602f531d9e9 Mon Sep 17 00:00:00 2001 From: Joris Wouter Jonkers Date: Tue, 20 Oct 2026 21:51:39 +0200 Subject: [PATCH 2/3] docs: the README sample as ruff formats it --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f4c778f..7a41bf6 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,16 @@ JWT access token against the Issuer's keys or introspects an opaque token, keepi ```python from jorisjonkers_commons.identity import ( - PrincipalReader, principal_middleware, require_recent_two_factor, require_role, + PrincipalReader, + principal_middleware, + require_recent_two_factor, + require_role, ) reader = PrincipalReader("https://auth.jorisjonkers.dev", "wiki", client_secret=secret) -app = principal_middleware(app, reader) # 401 when refused, 503 when identity is down -admin = require_role("admin")(admin_app) # 403 without the role -payments = require_recent_two_factor(5)(pay_app) # 303 to the Gate's step-up, 403 for tokens +app = principal_middleware(app, reader) # 401 when refused, 503 when identity is down +admin = require_role("admin")(admin_app) # 403 without the role +payments = require_recent_two_factor(5)(pay_app) # 303 to the Gate's step-up, 403 for tokens ``` The middleware and the helpers are plain ASGI, so they wrap a Starlette or FastAPI app or any one From 1362496430e152b6a92ae77c80bad9275b2ce12b Mon Sep 17 00:00:00 2001 From: Joris Wouter Jonkers Date: Tue, 20 Oct 2026 22:05:21 +0200 Subject: [PATCH 3/3] build: the contract at its 1.1.0 release --- contract/REF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract/REF b/contract/REF index 1a4e71e..dbbfd59 100644 --- a/contract/REF +++ b/contract/REF @@ -1 +1 @@ -37506c6 +contract/principal/v1.1.0